<?php

namespace Wpo\Services;

use WP_Error;

use Wpo\Core\WordPress_Helpers;
use Wpo\Services\Access_Token_Service;
use Wpo\Services\Log_Service;
use Wpo\Services\Options_Service;

// Prevent public access to this script
defined( 'ABSPATH' ) || die();

if ( ! class_exists( '\Wpo\Services\Graph_Service' ) ) {

	class Graph_Service {


		const REST_API           = 'https://graph.microsoft.com/';
		const GRAPH_VERSION      = 'v1.0';
		const GRAPH_VERSION_BETA = 'beta';

		/**
		 * Connects to Microsoft Graph REST api to get retrieve data on the basis of the query presented
		 *
		 * @since 0.1
		 *
		 * @param   string  $query  query part of the Graph query e.g. '/me/photo/$'.
		 * @param   string  $method HTTP Method (default GET).
		 * @param   boolean $binary Get binary data e.g. when getting user profile image.
		 * @param   array   $headers
		 * @param   boolean $use_delegated
		 * @param   boolean $prefetch Is deprecated since 11.0 when the method will figure out what it can use to obtain a delegated token.
		 * @param   string  $post_fields
		 * @param   string  $scope
		 * @return  array|WP_Error JSON string as associative array or false
		 */
		public static function fetch( $query, $method = 'GET', $binary = false, $headers = array(), $use_delegated = false, $prefetch = false, $post_fields = '', $scope = 'https://graph.microsoft.com/user.read', $log_level = 'WARN' ) {
			Log_Service::write_log( 'DEBUG', sprintf( '%s -> Requesting data from Microsoft Graph using query "%s" and scope "%s"', __METHOD__, $query, $scope ) );

			/**
			 * @since 10.0 it is possible to request data from Microsoft Graph
			 * using an app-only context.
			 */

			$use_b2c        = Options_Service::get_global_boolean_var( 'use_b2c' );
			$use_saml       = Options_Service::get_global_boolean_var( 'use_saml' );
			$multi_tenanted = Options_Service::get_global_boolean_var( 'multi_tenanted' );

			if ( $multi_tenanted ) {
				$request_service         = Request_Service::get_instance();
				$request                 = $request_service->get_request( $GLOBALS['WPO_CONFIG']['request_id'] );
				$id_token                = $request->get_item( 'id_token' );
				$oidc_user_is_logging_in = ! empty( $id_token );

				if ( $oidc_user_is_logging_in ) {
					$home_tenant_id = Options_Service::get_aad_option( 'tenant_id' );
					$user_tenant_id = ! empty( $id_token->tid ) ? $id_token->tid : '';

					if ( ! empty( $user_tenant_id ) && strcasecmp( $home_tenant_id, $user_tenant_id ) !== 0 ) {
						$use_delegated = true;
					}
				}
			}

			/**
			 * @since 24.3 To read the application secret expiration date MsGraphMailer may also fetch from Microsoft Graph.
			 *             When MsGraphMailer fetches from Microsoft Graph using this generic class it will be always using
			 *             application permissions because a delegated scenario is not possible. Therefore use_app_only will
			 *             also be set as true.
			 */

			$use_mail_config           = ! class_exists( '\Wpo\Login' ) && class_exists( '\Wpo\MsGraphMailer' );
			$use_app_only              = Options_Service::get_aad_option( 'use_app_only_token', true ) || ( $use_mail_config );
			$no_sso                    = Options_Service::get_global_boolean_var( 'no_sso' );
			$uses_me                   = WordPress_Helpers::stripos( $query, '/me/' ) !== false;
			$user_has_delegated_access = Access_Token_Service::user_has_delegated_access( get_current_user_id() );

			if ( ! $user_has_delegated_access && ! $use_app_only ) {
				$warning = 'Could not retrieve an access token for (scope|query) ' . $scope . '|' . $query . '.  Error details: The current user is either not logged in, did not sign in using OpenID Connect Single Sign-On (SSO), or WPO365 does not have the necessary permissions to access data on behalf of the organization. Please review the "Application Access" settings on the plugin\'s "Integration" configuration page.';
			}

			if ( $use_delegated && $no_sso ) {
				$warning = 'Could not retrieve an access token for (scope|query) ' . $scope . '|' . $query . '.  Error details: The plugin cannot retrieve a token with delegated API permissions when SSO has been disabled (request application-level permissions instead).';
			}

			if ( $uses_me && $no_sso ) {
				$warning = 'Could not retrieve an access token for (scope|query) ' . $scope . '|' . $query . '.  Error details: The plugin cannot execute a query for the /me endpoint when SSO has been disabled (request application-level permissions instead).';
			}

			if ( ! empty( $warning ) ) {
				Log_Service::write_log( $log_level, __METHOD__ . ' -> ' . $warning );
				return new \WP_Error( 'GraphFetchError', $warning );
			}

			if ( $use_delegated ) {
				$access_token = Access_Token_Service::get_access_token( $scope );
			} elseif ( $use_app_only ) {
				$scope_host     = WordPress_Helpers::stripos( $scope, 'https://' ) !== false ? wp_parse_url( $scope, PHP_URL_HOST ) : 'graph.microsoft.com';
				$tld            = $use_mail_config ? Options_Service::get_mail_option( 'mail_tld' ) : Options_Service::get_aad_option( 'tld' );
				$tld            = ! empty( $tld ) ? $tld : '.com';
				$scope_host     = str_replace( '.com', $tld, $scope_host );
				$app_only_scope = "https://$scope_host/.default";
				$scope_segments = explode( '/', $scope );
				$role           = array_pop( $scope_segments );

				if ( strcasecmp( $role, 'User.Read' ) === 0 ) {
					$role = 'User.Read.All';
				}

				$access_token = Access_Token_Service::get_app_only_access_token( $app_only_scope, $role, $use_mail_config );

				if ( is_wp_error( $access_token ) && $user_has_delegated_access ) {
					Log_Service::write_log( $log_level, sprintf( '%s -> no application role found to match scope %s for query %s therefore falling back to delegated permissions', __METHOD__, $scope, $query ) );
					$access_token = Access_Token_Service::get_access_token( $scope );
				}
			} elseif ( $use_b2c || $use_saml ) {
				$access_token = new WP_Error( 'TokenError', 'Could not retrieve a token with application-level permissions which is required when using "Azure AD B2C" or "SAML 2.0" based Single Sign-on and the administrator has configured features that request data from Microsoft Graph.' );
			} elseif ( $no_sso ) {
				$access_token = new WP_Error( 'TokenError', 'Could not retrieve a token with application-level permissions which is required when the Single Sign-on feature has been disabled and the administrator has configured features that request data from Microsoft Graph.' );
			} else {
				$access_token = Access_Token_Service::get_access_token( $scope );
			}

			if ( is_wp_error( $access_token ) ) {
				$warning = 'Could not retrieve an access token for (scope|query) ' . $scope . '|' . $query . '.  Error details: ' . $access_token->get_error_message();
				Log_Service::write_log( $log_level, __METHOD__ . ' -> ' . $warning );
				return new \WP_Error( $access_token->get_error_code(), $warning );
			}

			$_headers = array();

			if ( WordPress_Helpers::array_is_list_polyfill( $headers ) ) {

				foreach ( $headers as $header ) {
								$splitted = explode( ':', $header );

					if ( count( $splitted ) === 2 ) {
						$_headers[ WordPress_Helpers::trim( $splitted[0] ) ] = WordPress_Helpers::trim( $splitted[1] );
					}
				}
			} else {

				foreach ( $headers as $key => $value ) {
					$_headers[ WordPress_Helpers::trim( $key ) ] = WordPress_Helpers::trim( $value );
				}
			}

			$_headers['Authorization'] = sprintf( 'Bearer %s', $access_token->access_token );
			$_headers['Expect']        = '';

			/**
			 * @since 13.0
			 */

			if ( WordPress_Helpers::stripos( $query, '$count=true' ) !== false ) {
				$_headers['ConsistencyLevel'] = 'eventual';
			}

			$graph_version = Options_Service::get_global_string_var( 'graph_version' );
			$graph_version = empty( $graph_version ) || $graph_version === 'current'
			? self::GRAPH_VERSION
			: ( $graph_version === 'beta'
				? self::GRAPH_VERSION_BETA
				: self::GRAPH_VERSION
			);

			$url = self::REST_API . $graph_version . $query;

			$skip_ssl_verify = ! Options_Service::get_global_boolean_var( 'skip_host_verification' );

			Log_Service::write_log( 'DEBUG', __METHOD__ . ' -> Fetching from ' . $url );

			if ( WordPress_Helpers::stripos( $method, 'GET' ) === 0 ) {
				$response = wp_remote_get(
					$url,
					array(
						'method'    => 'GET',
						'headers'   => $_headers,
						'sslverify' => $skip_ssl_verify,
					)
				);
			} elseif ( WordPress_Helpers::stripos( $method, 'POST' ) === 0 ) {
				$response = wp_remote_post(
					$url,
					array(
						'body'      => $post_fields,
						'headers'   => $_headers,
						'sslverify' => $skip_ssl_verify,
					)
				);
			} elseif ( WordPress_Helpers::stripos( $method, 'PATCH' ) === 0 ) {
				$response = wp_remote_post(
					$url,
					array(
						'body'      => $post_fields,
						'method'    => 'PATCH',
						'headers'   => $_headers,
						'sslverify' => $skip_ssl_verify,
					)
				);
			} else {
				return new \WP_Error( 'NotImplementedException', 'Error occured whilst fetching from Microsoft Graph: Method ' . $method . ' not implemented' );
			}

			if ( is_wp_error( $response ) ) {
				$warning = 'Error occured whilst fetching from Microsoft Graph: ' . $response->get_error_message();
				Log_Service::write_log( $log_level, __METHOD__ . " -> $warning" );
				return new \WP_Error( '1040', $warning );
			}

			$body                = wp_remote_retrieve_body( $response );
			$http_code           = wp_remote_retrieve_response_code( $response );
			$http_status_message = wp_remote_retrieve_response_message( $response );

			if ( $http_code === 302 ) {
				$location = wp_remote_retrieve_header( $response, 'location' );
				return array(
					'payload'       => $location,
					'response_code' => $http_code,
				);
			}

			if ( ! $binary ) {
				$body = ( $http_code < 200 || $http_code > 299 ) && empty( $body ) ? $http_status_message : json_decode( $body, true );
			}

			return array(
				'payload'       => $body,
				'response_code' => $http_code,
			);
		}

		/**
		 * Quick test to see if the result fetched from Microsoft Graph is valid.
		 *
		 * @since 7.17
		 *
		 * @param mixed  $fetch_result
		 * @param string $message
		 * @param string $level
		 * @param mixed  $on_error
		 *
		 * @return bool True if valid otherwise false
		 */
		public static function is_fetch_result_ok( $fetch_result, $message, $level = 'ERROR', $on_error = null ) {
			Log_Service::write_log( 'DEBUG', '##### -> ' . __METHOD__ );

			if ( is_wp_error( $fetch_result ) ) {
				$log_message = $message . ' [Error: ' . $fetch_result->get_error_message() . ']';
				Log_Service::write_log( $level, __METHOD__ . ' -> ' . $log_message );

				if ( ! empty( $on_error ) ) {
					$on_error( $log_message );
				}

				return false;
			}

			if ( $fetch_result['response_code'] < 200 || $fetch_result['response_code'] > 299 ) {

				if ( is_array( $fetch_result ) && isset( $fetch_result['payload'] ) && isset( $fetch_result['payload']['error'] ) && isset( $fetch_result['payload']['error']['message'] ) ) {
					$error_code  = isset( $fetch_result['payload']['error']['code'] ) ? sprintf( ' | %s', $fetch_result['payload']['error']['code'] ) : '';
					$log_message = $message . ' [Error: ' . $fetch_result['payload']['error']['message'] . $error_code . ' ]';
					Log_Service::write_log( $level, __METHOD__ . ' -> ' . $log_message );

					if ( ! empty( $on_error ) ) {
						$on_error( $log_message );
					}

					return false;
				}

				Log_Service::write_log( $level, __METHOD__ . ' -> ' . $message . ' [See log for details]' );
				$output_log_level = $level === 'ERROR' ? 'WARN' : 'DEBUG';
				Log_Service::write_log( $output_log_level, $fetch_result );
				return false;
			}

			return true;
		}
	}
}
