<?php

namespace App\Support\Socialite;

use GuzzleHttp\RequestOptions;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

/**
 * Microsoft (Entra ID / "Sign in with Microsoft") OAuth2 provider.
 *
 * Why this is hand-rolled instead of pulling socialiteproviders/microsoft:
 *   - Many MailTrixy customers run on shared hosting (Plesk / cPanel)
 *     where `composer require` isn't available — adding a runtime
 *     composer dependency would block the System Update flow.
 *   - The self-hosted driver is ~80 lines and covers every flow
 *     SocialLoginController exercises today (redirect / callback /
 *     user lookup). Anything fancier (multi-tenant restrictions,
 *     custom claims) can be added as a follow-up without breaking
 *     existing installs.
 *
 * Endpoints — uses the `common` tenant so both personal Microsoft
 * accounts (live.com / hotmail.com / outlook.com) AND work/school
 * Entra accounts can sign in. Customers who want to restrict to a
 * single tenant can override the URLs by extending this class.
 *
 * Token + user info via Microsoft Graph:
 *   - Auth:      https://login.microsoftonline.com/common/oauth2/v2.0/authorize
 *   - Token:     https://login.microsoftonline.com/common/oauth2/v2.0/token
 *   - Profile:   https://graph.microsoft.com/v1.0/me
 *
 * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
 */
class MicrosoftProvider extends AbstractProvider implements ProviderInterface
{
    /**
     * Tenant scopes we ask for. `User.Read` gets us id + email + name.
     * `offline_access` is needed only if we later want to refresh the
     * token silently; harmless to request now.
     */
    protected $scopes = ['openid', 'profile', 'email', 'User.Read', 'offline_access'];

    protected $scopeSeparator = ' ';

    protected function getAuthUrl($state): string
    {
        return $this->buildAuthUrlFromBase(
            'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
            $state,
        );
    }

    protected function getTokenUrl(): string
    {
        return 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
    }

    /**
     * Hit Microsoft Graph for the signed-in user. Graph returns the
     * canonical id under `id`, email under `mail` (or `userPrincipalName`
     * for accounts without an explicit mail attribute), and the display
     * name under `displayName`.
     */
    protected function getUserByToken($token): array
    {
        $response = $this->getHttpClient()->get(
            'https://graph.microsoft.com/v1.0/me',
            [
                RequestOptions::HEADERS => [
                    'Accept'        => 'application/json',
                    'Authorization' => 'Bearer ' . $token,
                ],
            ],
        );

        return (array) json_decode((string) $response->getBody(), true);
    }

    protected function mapUserToObject(array $user): User
    {
        // Mail is sometimes null for personal Microsoft accounts —
        // fall back to userPrincipalName (which is the sign-in email
        // for those accounts) so we never hand back a null email and
        // crash the caller.
        $email = $user['mail']
            ?? $user['userPrincipalName']
            ?? $user['preferredUsername']
            ?? null;

        return (new User())->setRaw($user)->map([
            'id'       => $user['id']          ?? null,
            'nickname' => $user['displayName'] ?? null,
            'name'     => $user['displayName'] ?? null,
            'email'    => $email,
            'avatar'   => null, // Graph /me/photo requires a separate call we skip for now
        ]);
    }
}
