<?php
/**
 * Microsoft Graph sendMail via OAuth 2.0 (Mail.Send).
 *
 * @package EmailSmtp
 * @link https://learn.microsoft.com/en-us/graph/api/user-sendmail
 */

namespace EmailSmtp\Providers\Microsoft;

use EmailSmtp\Includes\ApiHttpClient;
use EmailSmtp\Includes\DeliveryResult;
use EmailSmtp\Includes\EmailMessage;
use EmailSmtp\Includes\ErrorNormalizer;
use EmailSmtp\Includes\OAuth\AbstractOAuthMailProvider;
use EmailSmtp\Includes\OAuth\OAuthState;
use EmailSmtp\Includes\OAuth\OAuthTokenStore;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class Provider extends AbstractOAuthMailProvider {

	private const GRAPH_SEND = 'https://graph.microsoft.com/v1.0/me/sendMail';
	private const GRAPH_ME   = 'https://graph.microsoft.com/v1.0/me';

	/** Exchange Online typical max recipients per message. */
	private const RECIPIENT_CAP = 500;

	public function get_id(): string {
		return 'microsoft';
	}

	public function get_name(): string {
		return __( 'Microsoft 365 / Outlook', 'wp-powersuite' );
	}

	public function get_description(): string {
		return __( 'Send through Microsoft Graph with delegated OAuth (Mail.Send). No mailbox password stored.', 'wp-powersuite' );
	}

	public function get_recommended_use(): string {
		return __( 'Microsoft 365 or Outlook.com mailboxes. Prefer a dedicated ESP for high-volume transactional mail.', 'wp-powersuite' );
	}

	public function get_docs_url(): string {
		return 'https://learn.microsoft.com/en-us/graph/api/user-sendmail';
	}

	public function get_icon(): string {
		return 'dashicons-email-alt2';
	}


	public function get_scopes(): array {
		return [
			'https://graph.microsoft.com/Mail.Send',
			'https://graph.microsoft.com/User.Read',
			'offline_access',
			'openid',
			'email',
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	private function tenant( array $settings ): string {
		$data   = OAuthTokenStore::read( $settings, 'microsoft' );
		$tenant = sanitize_text_field( (string) ( $data['tenant'] ?? 'common' ) );
		if ( ! preg_match( '/^[a-zA-Z0-9._-]+$/', $tenant ) ) {
			return 'common';
		}
		return $tenant;
	}

	private function authorize_endpoint( array $settings ): string {
		return 'https://login.microsoftonline.com/' . rawurlencode( $this->tenant( $settings ) ) . '/oauth2/v2.0/authorize';
	}

	private function token_endpoint( array $settings ): string {
		return 'https://login.microsoftonline.com/' . rawurlencode( $this->tenant( $settings ) ) . '/oauth2/v2.0/token';
	}

	public function get_authorize_url( array $settings, string $state, string $code_challenge ): string {
		$data = OAuthTokenStore::read( $settings, 'microsoft' );
		$args = [
			'client_id'             => $data['client_id'],
			'response_type'         => 'code',
			'redirect_uri'          => OAuthState::callback_url( 'microsoft' ),
			'response_mode'         => 'query',
			'scope'                 => implode( ' ', $this->get_scopes() ),
			'state'                 => $state,
			'code_challenge'        => $code_challenge,
			'code_challenge_method' => 'S256',
		];
		return $this->authorize_endpoint( $settings ) . '?' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 );
	}

	public function exchange_code( array $settings, string $code, string $code_verifier ) {
		$data     = OAuthTokenStore::read( $settings, 'microsoft' );
		$response = $this->post_token_form(
			$this->token_endpoint( $settings ),
			[
				'client_id'     => $data['client_id'],
				'client_secret' => $data['client_secret'],
				'code'          => $code,
				'redirect_uri'  => OAuthState::callback_url( 'microsoft' ),
				'grant_type'    => 'authorization_code',
				'code_verifier' => $code_verifier,
				'scope'         => implode( ' ', $this->get_scopes() ),
			]
		);

		if ( ! empty( $response['error'] ) || (int) $response['code'] < 200 || (int) $response['code'] > 299 || ! is_array( $response['json'] ) ) {
			return new \WP_Error( 'token_exchange', ApiHttpClient::extract_error_text( $response ) );
		}

		$json  = $response['json'];
		$token = (string) ( $json['access_token'] ?? '' );
		if ( $token === '' ) {
			return new \WP_Error( 'token_exchange', __( 'Microsoft did not return an access token.', 'wp-powersuite' ) );
		}

		return [
			'access_token'  => $token,
			'refresh_token' => (string) ( $json['refresh_token'] ?? '' ),
			'expires_in'    => (int) ( $json['expires_in'] ?? 3600 ),
			'email'         => $this->fetch_account_email( $token ),
		];
	}

	public function refresh_access_token( array $settings, string $refresh_token ) {
		$data     = OAuthTokenStore::read( $settings, 'microsoft' );
		$response = $this->post_token_form(
			$this->token_endpoint( $settings ),
			[
				'client_id'     => $data['client_id'],
				'client_secret' => $data['client_secret'],
				'refresh_token' => $refresh_token,
				'grant_type'    => 'refresh_token',
				'scope'         => implode( ' ', $this->get_scopes() ),
			]
		);

		if ( ! empty( $response['error'] ) || (int) $response['code'] < 200 || (int) $response['code'] > 299 || ! is_array( $response['json'] ) ) {
			return new \WP_Error( 'token_refresh', ApiHttpClient::extract_error_text( $response ) );
		}

		$json  = $response['json'];
		$token = (string) ( $json['access_token'] ?? '' );
		if ( $token === '' ) {
			return new \WP_Error( 'token_refresh', __( 'Microsoft token refresh failed.', 'wp-powersuite' ) );
		}

		return [
			'access_token'  => $token,
			'refresh_token' => (string) ( $json['refresh_token'] ?? '' ),
			'expires_in'    => (int) ( $json['expires_in'] ?? 3600 ),
		];
	}

	public function fetch_account_email( string $access_token ): string {
		$response = ApiHttpClient::request(
			'GET',
			self::GRAPH_ME . '?$select=mail,userPrincipalName,displayName',
			[ 'Authorization' => 'Bearer ' . $access_token ]
		);
		if ( (int) $response['code'] >= 200 && (int) $response['code'] < 300 && is_array( $response['json'] ) ) {
			$email = sanitize_email( (string) ( $response['json']['mail'] ?? '' ) );
			if ( ! is_email( $email ) ) {
				$email = sanitize_email( (string) ( $response['json']['userPrincipalName'] ?? '' ) );
			}
			return is_email( $email ) ? $email : '';
		}
		return '';
	}

	public function send( EmailMessage $message, array $settings ): DeliveryResult {
		$missing = $this->require_from( $message );
		if ( $missing ) {
			return $missing;
		}

		$token = $this->get_valid_access_token( $settings );
		if ( is_wp_error( $token ) ) {
			$result = ErrorNormalizer::from_mixed( $token );
			return DeliveryResult::failure(
				$result->get_user_message(),
				$result->get_technical_message(),
				'authentication',
				'auth_expired',
				false,
				__( 'Disconnect and Connect the Microsoft account again. Confirm Mail.Send (delegated) and admin consent if required.', 'wp-powersuite' )
			);
		}

		$remaining = self::RECIPIENT_CAP;
		$to        = $this->take_recipients( $message->get_to(), $remaining );
		if ( $to === [] ) {
			return DeliveryResult::failure( __( 'At least one recipient is required.', 'wp-powersuite' ), 'missing_to', 'recipient' );
		}

		$from = $this->from_parts( $message );
		$body_content = $message->is_html()
			? ( $message->get_body() !== '' ? $message->get_body() : ' ' )
			: $this->plain_text_body( $message );

		$graph_message = [
			'subject'      => $message->get_subject() !== '' ? $message->get_subject() : '(no subject)',
			'body'         => [
				'contentType' => $message->is_html() ? 'HTML' : 'Text',
				'content'     => $body_content,
			],
			'toRecipients' => array_map(
				static fn( string $email ) => [ 'emailAddress' => [ 'address' => $email ] ],
				$to
			),
		];

		$cc = $this->take_recipients( $message->get_cc(), $remaining );
		if ( $cc !== [] ) {
			$graph_message['ccRecipients'] = array_map(
				static fn( string $email ) => [ 'emailAddress' => [ 'address' => $email ] ],
				$cc
			);
		}
		$bcc = $this->take_recipients( $message->get_bcc(), $remaining );
		if ( $bcc !== [] ) {
			$graph_message['bccRecipients'] = array_map(
				static fn( string $email ) => [ 'emailAddress' => [ 'address' => $email ] ],
				$bcc
			);
		}
		if ( is_email( $message->get_reply_to_email() ) ) {
			$graph_message['replyTo'] = [
				[
					'emailAddress' => array_filter(
						[
							'address' => $message->get_reply_to_email(),
							'name'    => $message->get_reply_to_name() !== '' ? $message->get_reply_to_name() : null,
						]
					),
				],
			];
		}

		$connected = (string) ( OAuthTokenStore::read( $settings, 'microsoft' )['connected_email'] ?? '' );
		if ( is_email( $from['email'] ) && ( $connected === '' || strcasecmp( $from['email'], $connected ) === 0 ) ) {
			$graph_message['from'] = [
				'emailAddress' => array_filter(
					[
						'address' => $from['email'],
						'name'    => $from['name'] !== '' ? $from['name'] : null,
					]
				),
			];
		}

		$x_headers = [];
		foreach ( $this->safe_custom_headers( $message ) as $name => $value ) {
			if ( ! str_starts_with( strtolower( (string) $name ), 'x-' ) ) {
				continue;
			}
			$x_headers[] = [
				'name'  => (string) $name,
				'value' => (string) $value,
			];
		}
		if ( $x_headers !== [] ) {
			$graph_message['internetMessageHeaders'] = $x_headers;
		}

		$attachments = $this->attachments_as_base64( $message );
		if ( $attachments !== [] ) {
			$graph_message['attachments'] = array_map(
				static function ( array $a ) {
					return [
						'@odata.type'  => '#microsoft.graph.fileAttachment',
						'name'         => $a['name'],
						'contentType'  => $a['type'] !== '' ? $a['type'] : 'application/octet-stream',
						'contentBytes' => $a['content'],
					];
				},
				$attachments
			);
		}

		$response = ApiHttpClient::request(
			'POST',
			self::GRAPH_SEND,
			[
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			],
			[
				'message'         => $graph_message,
				'saveToSentItems' => true,
			]
		);

		return ApiHttpClient::to_delivery_result(
			$response,
			static function () {
				return [
					'message' => __( 'Microsoft Graph accepted the message.', 'wp-powersuite' ),
					'id'      => '',
				];
			}
		);
	}
}
