<?php

namespace App\Mail\Transport;

use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * Microsoft Graph API transport for Outlook / Office365, authenticated with
 * an OAuth2 refresh token (no official Symfony bridge exists).
 *
 * Unlike Google, Microsoft's identity platform *rotates* refresh tokens: a
 * successful token refresh both issues a new refresh token and invalidates
 * the old one. If it isn't persisted immediately, the next send fails. The
 * optional $onTokenRefreshed callback (wired up in SmtpConfigService) is how
 * this stateless transport gets the new token back into the database.
 */
class MicrosoftApiTransport extends AbstractApiTransport
{
    private const TENANT        = 'common';
    private const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/' . self::TENANT . '/oauth2/v2.0/token';
    private const SEND_ENDPOINT  = 'https://graph.microsoft.com/v1.0/me/sendMail';
    private const SCOPES         = 'offline_access https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/User.Read';

    /** @param ?callable(array):void $onTokenRefreshed */
    public function __construct(
        private readonly string $clientId,
        private readonly string $clientSecret,
        private readonly ?string $refreshToken,
        private $onTokenRefreshed = null,
        ?\Symfony\Contracts\HttpClient\HttpClientInterface $client = null,
        ?\Psr\EventDispatcher\EventDispatcherInterface $dispatcher = null,
        ?\Psr\Log\LoggerInterface $logger = null,
    ) {
        parent::__construct($client, $dispatcher, $logger);
    }

    public function __toString(): string
    {
        return 'outlook+api://graph.microsoft.com';
    }

    protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
    {
        if (!$this->refreshToken) {
            throw new HttpTransportException('This Outlook connection has not been authenticated with Microsoft yet - click "Authenticate with Office365" first.');
        }

        $accessToken = $this->fetchAccessToken();

        $response = $this->client->request('POST', self::SEND_ENDPOINT, [
            'auth_bearer' => $accessToken,
            'json'        => [
                'message'         => $this->getMessagePayload($email, $envelope),
                'saveToSentItems' => 'true',
            ],
        ]);

        try {
            $statusCode = $response->getStatusCode();
        } catch (TransportExceptionInterface $e) {
            throw new HttpTransportException('Could not reach the Microsoft Graph API.', $response, 0, $e);
        }

        // sendMail returns 202 Accepted with an empty body on success - no message id to capture.
        if (!in_array($statusCode, [200, 202], true)) {
            $result = [];
            try {
                $result = $response->toArray(false);
            } catch (\Throwable) {
                // fall through to the raw body below
            }

            $message = $result['error']['message'] ?? $response->getContent(false);

            throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $message, $statusCode), $response);
        }

        return $response;
    }

    private function getMessagePayload(Email $email, Envelope $envelope): array
    {
        $addr = fn (Address $a) => ['emailAddress' => array_filter([
            'address' => $a->getAddress(),
            'name'    => $a->getName() ?: null,
        ])];

        $html = $email->getHtmlBody();
        $body = null !== $html
            ? ['contentType' => 'HTML', 'content' => $html]
            : ['contentType' => 'Text', 'content' => $email->getTextBody() ?? ''];

        $message = [
            'subject'      => $email->getSubject() ?? '',
            'body'         => $body,
            'from'         => $addr($envelope->getSender()),
            'toRecipients' => array_map($addr, $this->getRecipients($email, $envelope)),
        ];

        if ($cc = $email->getCc()) {
            $message['ccRecipients'] = array_map($addr, $cc);
        }
        if ($bcc = $email->getBcc()) {
            $message['bccRecipients'] = array_map($addr, $bcc);
        }
        if ($replyTo = $email->getReplyTo()) {
            $message['replyTo'] = array_map($addr, $replyTo);
        }

        if ($attachments = $email->getAttachments()) {
            $message['attachments'] = array_map(fn (DataPart $a) => [
                '@odata.type'  => '#microsoft.graph.fileAttachment',
                'name'         => $a->getFilename() ?? 'attachment',
                'contentType'  => $a->getContentType(),
                'contentBytes' => base64_encode($a->getBody()),
            ], $attachments);
        }

        return $message;
    }

    private function fetchAccessToken(): string
    {
        $response = $this->client->request('POST', self::TOKEN_ENDPOINT, [
            'body' => [
                'client_id'     => $this->clientId,
                'client_secret' => $this->clientSecret,
                'refresh_token' => $this->refreshToken,
                'grant_type'    => 'refresh_token',
                'scope'         => self::SCOPES,
            ],
        ]);

        try {
            $result = $response->toArray(false);
        } catch (TransportExceptionInterface $e) {
            throw new HttpTransportException('Could not reach Microsoft\'s OAuth token endpoint.', $response, 0, $e);
        }

        if (empty($result['access_token'])) {
            $message = $result['error_description'] ?? $result['error'] ?? 'unknown error';

            throw new HttpTransportException("Unable to refresh the Microsoft access token: {$message}. Try re-authenticating with Microsoft.", $response);
        }

        // Microsoft rotates refresh tokens on every use - persist the new one now,
        // or the next send will fail with the now-invalidated old token.
        if ($this->onTokenRefreshed && !empty($result['refresh_token'])) {
            ($this->onTokenRefreshed)([
                'refresh_token' => $result['refresh_token'],
                'access_token'  => $result['access_token'],
                'expires_at'    => now()->addSeconds((int) ($result['expires_in'] ?? 3600))->toISOString(),
            ]);
        }

        return $result['access_token'];
    }
}
