<?php

declare(strict_types=1);

namespace PpPhp\Core\Mail;

/**
 * MailOAuth — OAuth 2.0 "sign in with Google / Microsoft" for SMTP sending.
 *
 * This is the modern replacement for mailbox passwords: Gmail and Microsoft 365
 * increasingly require OAuth for SMTP. The owner registers an OAuth app once
 * (client id + secret) and connects the mailbox; we keep only the long-lived
 * REFRESH token (encrypted) and mint a short-lived access token on demand, which
 * the SMTP engine presents via AUTH XOAUTH2 (see Email::attempt()).
 *
 * Mirrors core/Storage/StorageOAuth (same dance, different scopes/endpoints).
 */
final class MailOAuth
{
    /** provider => endpoints, scope, and the SMTP server it authenticates. */
    public const PROVIDERS = [
        'google' => [
            'label' => 'Google / Gmail',
            'auth'  => 'https://accounts.google.com/o/oauth2/v2/auth',
            'token' => 'https://oauth2.googleapis.com/token',
            // mail.google.com = full SMTP/IMAP send scope; openid+email to learn the address.
            'scope' => 'https://mail.google.com/ openid email',
            'extra' => ['access_type' => 'offline', 'prompt' => 'consent'],
            'smtp'  => ['host' => 'smtp.gmail.com', 'port' => 587, 'encryption' => 'tls'],
        ],
        'microsoft' => [
            'label' => 'Microsoft / Outlook',
            'auth'  => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
            'token' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
            // offline_access → refresh token; SMTP.Send → send mail; openid email → address.
            'scope' => 'offline_access openid email https://outlook.office365.com/SMTP.Send',
            'extra' => ['prompt' => 'consent'],
            'smtp'  => ['host' => 'smtp.office365.com', 'port' => 587, 'encryption' => 'tls'],
        ],
    ];

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

    public static function isProvider(string $p): bool
    {
        return isset(self::PROVIDERS[$p]);
    }

    public static function label(string $provider): string
    {
        return (string) (self::PROVIDERS[$provider]['label'] ?? $provider);
    }

    /** SMTP server (host/port/encryption) for a provider. */
    public static function smtp(string $provider): array
    {
        return (array) (self::PROVIDERS[$provider]['smtp'] ?? []);
    }

    /** The consent URL to send the owner to (step 1 of connecting a mailbox). */
    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
     * (refresh_token / access_token / id_token / expires_in), 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;
        $body = [
            'client_id'    => $clientId,
            'code'         => $code,
            'grant_type'   => 'authorization_code',
            'redirect_uri' => $redirectUri,
        ];
        if ($clientSecret !== '') $body['client_secret'] = $clientSecret;
        if ($provider === 'microsoft') $body['scope'] = $p['scope'];
        return self::tokenRequest($p['token'], $body);
    }

    /** A fresh access token from a stored refresh token, cached per request. '' on failure. */
    public static function accessToken(string $provider, string $clientId, string $clientSecret, string $refreshToken): string
    {
        $p = self::PROVIDERS[$provider] ?? null;
        if ($p === null || $refreshToken === '' || $clientId === '') return '';
        $ck  = $provider . ':' . md5($refreshToken . $clientId);
        $now = time();
        if (isset(self::$cache[$ck]) && self::$cache[$ck]['exp'] > $now + 30) {
            return self::$cache[$ck]['token'];
        }
        $body = [
            'client_id'     => $clientId,
            'refresh_token' => $refreshToken,
            'grant_type'    => 'refresh_token',
        ];
        if ($clientSecret !== '') $body['client_secret'] = $clientSecret;
        if ($provider === 'microsoft') $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;
    }

    /**
     * Pull the mailbox address out of an OpenID id_token (no extra HTTP call).
     * Google puts it in `email`; Microsoft in `preferred_username` (or `email`).
     */
    public static function emailFromIdToken(string $idToken): string
    {
        $parts = explode('.', $idToken);
        if (count($parts) < 2) return '';
        $payload = json_decode((string) base64_decode(strtr($parts[1], '-_', '+/')), true);
        if (!is_array($payload)) return '';
        foreach (['email', 'preferred_username', 'upn'] as $claim) {
            $v = (string) ($payload[$claim] ?? '');
            if ($v !== '' && filter_var($v, FILTER_VALIDATE_EMAIL)) return $v;
        }
        return '';
    }

    /** @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;
    }
}
