<?php
/**
 * OAuth provider endpoint + scope configuration.
 *
 * @package NimblyMail\SMTP
 */

namespace NimblyMail\SMTP\OAuth;

defined( 'ABSPATH' ) || exit;

/**
 * Class Providers
 *
 * Static config for Gmail (google) and Outlook (microsoft) OAuth flows.
 *
 * Mirrors the upstream contract the centralized NimblyMail OAuth Worker
 * (oauth.nimblymail.com) currently enforces, so BYOK callers that connect
 * directly to Google / Microsoft see the same authorization scopes and
 * token-endpoint behavior as proxy-backed installs.
 *
 * The Microsoft entries are templates because the v2.0 token+authorize URLs
 * include the tenant id ('common' | 'organizations' | 'consumers' | GUID).
 *
 * @since 1.0.0
 */
class Providers {

    /**
     * Hardcoded provider config, keyed by provider slug.
     *
     * @since 1.0.0
     *
     * @var array<string, array<string, mixed>>
     */
    private static array $config = array(
        'google'    => array(
            'auth_url'          => 'https://accounts.google.com/o/oauth2/v2/auth',
            'token_url'         => 'https://oauth2.googleapis.com/token',
            'revoke_url'        => 'https://oauth2.googleapis.com/revoke',
            'scopes'            => array( 'https://mail.google.com/' ),
            // access_type=offline + prompt=consent guarantee a refresh_token even on re-auth.
            'extra_auth_params' => array(
                'access_type' => 'offline',
                'prompt'      => 'consent',
            ),
        ),
        'microsoft' => array(
            'auth_url_template'  => 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize',
            'token_url_template' => 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
            // Microsoft v2.0 has no token-revoke endpoint; DirectClient::revoke() degrades to local-only.
            'revoke_url'         => null,
            // offline_access is non-removable — without it Microsoft refuses to issue a refresh_token.
            'scopes'             => array(
                'https://outlook.office.com/SMTP.Send',
                'offline_access',
                'openid',
                'email',
            ),
            // prompt=consent forces the consent screen even when the user
            // has previously granted the app. Without it Microsoft may
            // silently re-use cached consent, skip the offline_access grant
            // step, and return no refresh_token — leaving BYOK with a
            // 1-hour usable window before SMTP starts 401-ing with no
            // diagnostic. Matches the proxy's behavior for parity.
            'extra_auth_params'  => array(
                'prompt' => 'consent',
            ),
        ),
    );

    /**
     * Whether a provider slug is recognized.
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     *
     * @return bool
     */
    public static function exists( string $provider ): bool {
        return array_key_exists( $provider, self::$config );
    }

    /**
     * Authorization endpoint URL for the provider (with tenant interpolated).
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     * @param string $tenant   Microsoft tenant identifier; ignored for Google.
     *
     * @return string
     */
    public static function auth_url( string $provider, string $tenant = 'common' ): string {
        $cfg = self::$config[ $provider ] ?? array();
        if ( isset( $cfg['auth_url'] ) ) {
            return (string) $cfg['auth_url'];
        }
        if ( isset( $cfg['auth_url_template'] ) ) {
            return str_replace( '{tenant}', self::normalize_tenant( $tenant ), (string) $cfg['auth_url_template'] );
        }
        return '';
    }

    /**
     * Token endpoint URL for the provider (with tenant interpolated).
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     * @param string $tenant   Microsoft tenant identifier; ignored for Google.
     *
     * @return string
     */
    public static function token_url( string $provider, string $tenant = 'common' ): string {
        $cfg = self::$config[ $provider ] ?? array();
        if ( isset( $cfg['token_url'] ) ) {
            return (string) $cfg['token_url'];
        }
        if ( isset( $cfg['token_url_template'] ) ) {
            return str_replace( '{tenant}', self::normalize_tenant( $tenant ), (string) $cfg['token_url_template'] );
        }
        return '';
    }

    /**
     * Revocation endpoint URL or null when the provider has none.
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     *
     * @return string|null
     */
    public static function revoke_url( string $provider ): ?string {
        $cfg = self::$config[ $provider ] ?? array();
        if ( ! array_key_exists( 'revoke_url', $cfg ) ) {
            return null;
        }
        $value = $cfg['revoke_url'];
        return null === $value ? null : (string) $value;
    }

    /**
     * Default scopes for the provider, joined with a single space.
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     *
     * @return string
     */
    public static function scopes( string $provider ): string {
        $cfg    = self::$config[ $provider ] ?? array();
        $scopes = isset( $cfg['scopes'] ) && is_array( $cfg['scopes'] ) ? $cfg['scopes'] : array();
        return implode( ' ', array_map( 'strval', $scopes ) );
    }

    /**
     * Extra query parameters to append to the authorize URL.
     *
     * @since 1.0.0
     *
     * @param string $provider Provider slug.
     *
     * @return array<string, string>
     */
    public static function extra_auth_params( string $provider ): array {
        $cfg   = self::$config[ $provider ] ?? array();
        $extra = isset( $cfg['extra_auth_params'] ) && is_array( $cfg['extra_auth_params'] ) ? $cfg['extra_auth_params'] : array();
        $out   = array();
        foreach ( $extra as $k => $v ) {
            $out[ (string) $k ] = (string) $v;
        }
        return $out;
    }

    /**
     * The authorized redirect URI BYOK installs register with Google / Microsoft.
     *
     * Matches the proxy's `callback_url` verbatim so a single
     * {@see \NimblyMail\SMTP\Rest\OAuthController::maybe_handle_callback()}
     * listener catches both hosted and BYOK callbacks.
     *
     * @since 1.0.0
     *
     * @return string
     */
    public static function default_redirect_uri(): string {
        return admin_url( 'admin.php?page=nimblymail-smtp-settings' );
    }

    /**
     * Normalize a Microsoft tenant identifier to one of the accepted forms.
     *
     * Accepted values: 'common' (any account), 'organizations' (AAD-only),
     * 'consumers' (personal Microsoft accounts only), or a 36-character
     * tenant GUID. Falls back to 'common' on anything else.
     *
     * @since 1.0.0
     *
     * @param string $tenant Raw tenant value.
     *
     * @return string
     */
    public static function normalize_tenant( string $tenant ): string {
        $tenant = trim( strtolower( $tenant ) );
        if ( in_array( $tenant, array( 'common', 'organizations', 'consumers' ), true ) ) {
            return $tenant;
        }
        if ( preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $tenant ) === 1 ) {
            return $tenant;
        }
        return 'common';
    }
}
