<?php

declare(strict_types=1);

namespace PpPhp\Core\Storage;

/**
 * StorageOAuth — the OAuth2 refresh-token dance shared by the Google Drive and
 * OneDrive drivers, plus the authorize-URL / code-exchange helpers used by the
 * console's "Connect account" flow.
 *
 * The owner registers an OAuth app once (client id + secret) and connects an
 * account; we keep only the long-lived REFRESH token (encrypted, via
 * StorageConfig) and mint a short-lived access token on demand — cached in
 * memory per request so a page that resolves many file URLs refreshes once.
 */
final class StorageOAuth
{
    /** provider => endpoints + scope. */
    public const PROVIDERS = [
        'gdrive' => [
            'auth'  => 'https://accounts.google.com/o/oauth2/v2/auth',
            'token' => 'https://oauth2.googleapis.com/token',
            'scope' => 'https://www.googleapis.com/auth/drive.file',
            'extra' => ['access_type' => 'offline', 'prompt' => 'consent'],
        ],
        'onedrive' => [
            'auth'  => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
            'token' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
            'scope' => 'offline_access Files.ReadWrite',
            'extra' => [],
        ],
    ];

    /** @var array<string,array{token:string,exp:int}> in-request access-token cache. */
    private static array $cache = [];

    /** The consent URL to send the owner to (step 1 of connecting an account). */
    public static function authorizeUrl(string $provider, string $clientId, string $redirectUri, string $state): string
    {
        $p = self::PROVIDERS[$provider] ?? null;
        if ($p === null) return '';
        $q = array_merge([
            'client_id' => $clientId,
            'response_type' => 'code',
            'redirect_uri' => $redirectUri,
            'scope' => $p['scope'],
            'state' => $state,
        ], $p['extra']);
        return $p['auth'] . '?' . http_build_query($q, '', '&', PHP_QUERY_RFC3986);
    }

    /**
     * Exchange the authorization code for tokens (step 2). Returns the token
     * payload including refresh_token (store that), or null on failure.
     *
     * @return array<string,mixed>|null
     */
    public static function exchangeCode(string $provider, string $clientId, string $clientSecret, string $code, string $redirectUri): ?array
    {
        $p = self::PROVIDERS[$provider] ?? null;
        if ($p === null) return null;
        return self::tokenRequest($p['token'], [
            'client_id' => $clientId,
            'client_secret' => $clientSecret,
            'code' => $code,
            'grant_type' => 'authorization_code',
            'redirect_uri' => $redirectUri,
        ]);
    }

    /** A fresh access token for a connection (refresh-token grant), cached per request. */
    public static function accessToken(string $provider, array $conn): string
    {
        $refresh = (string) ($conn['refreshToken'] ?? '');
        $clientId = (string) ($conn['clientId'] ?? '');
        $clientSecret = (string) ($conn['clientSecret'] ?? '');
        if ($refresh === '' || $clientId === '') return '';
        $ck = $provider . ':' . md5($refresh . $clientId);
        $now = time();
        if (isset(self::$cache[$ck]) && self::$cache[$ck]['exp'] > $now + 30) {
            return self::$cache[$ck]['token'];
        }
        $p = self::PROVIDERS[$provider] ?? null;
        if ($p === null) return '';
        $body = [
            'client_id' => $clientId,
            'refresh_token' => $refresh,
            'grant_type' => 'refresh_token',
        ];
        if ($clientSecret !== '') $body['client_secret'] = $clientSecret;
        if ($provider === 'onedrive') $body['scope'] = $p['scope'];
        $res = self::tokenRequest($p['token'], $body);
        $token = (string) ($res['access_token'] ?? '');
        if ($token !== '') {
            self::$cache[$ck] = ['token' => $token, 'exp' => $now + (int) ($res['expires_in'] ?? 3600)];
        }
        return $token;
    }

    /** @return array<string,mixed>|null */
    private static function tokenRequest(string $url, array $form): ?array
    {
        if (!function_exists('curl_init')) return null;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POSTFIELDS => http_build_query($form),
            CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'],
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 30,
        ]);
        $resp = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status < 200 || $status >= 300 || !is_string($resp)) return null;
        $json = json_decode($resp, true);
        return is_array($json) ? $json : null;
    }
}
