<?php

namespace Drupal\user_provisioning\ProviderSpecific\APIHandler\UserAPIHandler;

use Drupal\azure_ad\moAzureConstants;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\user_provisioning\Helpers\moUserProvisioningLogger;
use Drupal\user_provisioning\ProviderSpecific\APIHandler\moAPIHandlerInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;

/**
 * Handles Azure AD API requests for user provisioning.
 */
class moUserAzureAPIHandler implements moAPIHandlerInterface {
  /**
   * Azure token endpoint URL template.
   *
   * @var string
   *   Token endpoint URL template.
   */
  public string $tokenEndpoint;

  /**
   * Azure user info endpoint URL.
   *
   * @var string
   *   User info endpoint URL.
   */
  public string $userinfoEndpoint;

  /**
   * OAuth scope for Azure token requests.
   *
   * @var string
   *   OAuth scope value.
   */
  public string $scope;

  /**
   * Azure client ID.
   *
   * @var string
   *   Azure application client ID.
   */
  private string $clientId;

  /**
   * Azure client secret.
   *
   * @var string
   *   Azure application client secret.
   */
  private string $clientSecret;

  /**
   * Azure tenant identifier.
   *
   * @var string
   *   Azure tenant ID.
   */
  private string $tenant;

  /**
   * UPN used for test profile lookups.
   *
   * @var string
   *   Test UPN identifier.
   */
  private string $upnId;

  /**
   * Cached Azure access token.
   *
   * @var mixed
   *   Current OAuth access token.
   */
  private mixed $accessToken;

  /**
   * HTTP client for Azure API requests.
   *
   * @var \GuzzleHttp\Client
   *   HTTP client instance.
   */
  private Client $httpClient;

  /**
   * Immutable Azure AD configuration.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   *   Read-only configuration object.
   */
  private ImmutableConfig $config;

  /**
   * Logger for handler diagnostics.
   *
   * @var \Drupal\user_provisioning\Helpers\moUserProvisioningLogger
   *   Logger instance.
   */
  private moUserProvisioningLogger $moLogger;

  /**
   * Constructs the Azure API handler.
   */
  public function __construct() {
    $this->config = \Drupal::config('azure_ad.settings');
    $this->tokenEndpoint = moAzureConstants::TOKEN_ENDPOINT;
    $this->userinfoEndpoint = moAzureConstants::USERINFO_ENDPOINT;
    $this->scope = moAzureConstants::SCOPE;
    $this->clientId = $this->config->get('mo_azure_application_id');
    $this->clientSecret = $this->config->get('mo_azure_application_secret');
    $this->tenant = $this->config->get('mo_azure_tenant_id');
    $this->upnId = $this->config->get('mo_azure_test_upn');
    $this->httpClient = \Drupal::httpClient();
    $this->accessToken = $this->moAzureGetAccessToken();
    $this->moLogger = new moUserProvisioningLogger();
  }

  /**
   * Fetches Azure user details for the configured test account.
   */
  public function moAzureGetUserDetails() {
    $user_info_url = $this->userinfoEndpoint . urlencode($this->upnId);
    $token = Json::decode($this->accessToken);
    if (isset($token['%error']) && is_string($token['%error'])) {
      $decodedError = Json::decode($token['%error']);
      return [
        "error_code" => $decodedError['error'] ?? '',
        "error_description" => $decodedError['error_description'] ?? '',
      ];
    }
    $resourceOwner = $this->moAzureGetResourceOwner($user_info_url, $this->accessToken);
    $resourceOwner['manager'] = $this->moAzureGetResourceOwner($user_info_url . '/manager', $this->accessToken, TRUE);
    return $resourceOwner;
  }

  /**
   * Retrieves an Azure access token using client credentials.
   */
  private function moAzureGetAccessToken() {
    $access_token = NULL;
    $token_endpoint = str_replace('{tenant}', $this->tenant, $this->tokenEndpoint);

    $response = $this->callService($token_endpoint,
          'grant_type=client_credentials&client_id=' . urlencode($this->clientId) . '&client_secret=' . urlencode($this->clientSecret) . '&scope=' . $this->scope,
          ['Content-Type' => 'application/x-www-form-urlencoded']
      );

    $content = Json::decode($response);

    // Step 1: Extract inner JSON.
    if (isset($content['%error']) && is_string($content['%error'])) {
      return $response;
    }
    elseif (isset($content["access_token"])) {
      $access_token = $content["access_token"];
    }
    else {
      exit('Invalid response received from OAuth Provider. Contact your administrator for more details.');
    }

    return $access_token;
  }

  /**
   * Fetches a user's profile image from Azure.
   */
  public function moAzureGetProfilePic($upn) {
    $profile_pic_url = $this->userinfoEndpoint . urlencode($upn) . '/photo/$value';
    return $this->moAzureGetMedia($profile_pic_url, $this->accessToken);
  }

  /**
   * Fetches media content from Azure endpoints.
   */
  private function moAzureGetMedia($url, $access_token) {
    $response = $this->callService($url,
          NULL,
          ['Content-Type' => 'image/jpg', 'Authorization' => 'Bearer ' . $access_token],
          'GET', TRUE
      );

    if (is_null($response)) {
      return NULL;
    }
    else {
      return base64_encode($response->getContents());
    }
  }

  /**
   * Executes HTTP requests against Azure APIs.
   */
  private function callService($url, $fields, $header = FALSE, $get_post = '', $is_manager = FALSE) {
    if (!$this->isCurlInstalled()) {
      return Json::encode([
        "statusCode" => 'ERROR',
        "statusMessage" => 'cURL is not enabled on your site. Please enable the cURL module.',
      ]);
    }
    $fieldString = is_string($fields) ? $fields : Json::encode($fields);

    if ($get_post == 'GET') {
      try {
        $response = $this->httpClient
          ->get($url, [
            'headers' => $header,
          ]);
        return $response->getBody();
      }
      catch (\Exception $exception) {
        $error = $exception->getResponse()->getBody()->getContents();
        \Drupal::logger('azure_ad')->notice('Error: <pre><code>' . print_r($error, TRUE) . '</code></pre>');
        if (!$is_manager) {
          return $error;
        }
      }
    }
    else {
      try {
        $response = $this->httpClient
          ->post($url, [
            'body' => $fieldString,
            'allow_redirects' => TRUE,
            'decode_content' => TRUE,
            'headers' => $header,
          ]);
        return $response->getBody()->getContents();
      }
      catch (RequestException $exception) {
        $error = [
          '%error' => $exception->getResponse()->getBody()->getContents(),
        ];
        \Drupal::logger('azure_ad')->notice('Error:  %error', $error);
        return Json::encode($error);
      }
    }
    return NULL;
  }

  /**
   * Checks whether cURL is available in the environment.
   */
  private function isCurlInstalled(): bool {
    return in_array('curl', get_loaded_extensions());
  }

  /**
   * Fetches resource owner details from Azure.
   */
  private function moAzureGetResourceOwner($resource_owner_details_url, $access_token, $is_manager = FALSE) {
    $response = $this->callService($resource_owner_details_url,
          NULL,
          ['Authorization' => 'Bearer ' . $access_token],
          'GET', $is_manager
      );

    return $this->checkResponseIfAnyErrors($response);
  }

  /**
   * Decodes Azure response content when available.
   */
  public function checkResponseIfAnyErrors($response) {
    if (isset($response) && !empty($response)) {
      return Json::decode($response);
    }
    return NULL;
  }

  /**
   * {@inheritDoc}
   */
  public function get($resource_id) {
    $url = str_replace('resource_id', $resource_id, moAzureConstants::CHECK_USER);
    $this->moLogger->addLog('Query url is ' . $url, __LINE__, __FUNCTION__, __FILE__);

    $options = [
      'headers' => [
        'Authorization' => 'Bearer ' . $this->accessToken,
        'Content-Type' => 'application/json',
      ],
    ];

    $this->moLogger->addLog('Query url is ' . $url, __LINE__, __FUNCTION__, __FILE__);
    $this->moLogger->addFormattedLog($options, __LINE__, __FUNCTION__, __FILE__, 'The header for resource search request is:');

    try {
      return $this->httpClient->get($url, $options);
    }
    catch (\Exception $exception) {
      throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
    }
  }

  /**
   * {@inheritDoc}
   */
  public function post(array $body) {
    $header = [
      'Authorization' => 'Bearer ' . $this->accessToken,
      'Content-Type' => 'application/json',
    ];

    $options = [
      'headers' => $header,
      'body' => Json::encode($body),
    ];

    $this->moLogger->addLog('Query url is ' . $this->userinfoEndpoint, __LINE__, __FUNCTION__, __FILE__);
    $this->moLogger->addFormattedLog($options, __LINE__, __FUNCTION__, __FILE__, 'The header and body for resource creation request is:');

    try {
      return $this->httpClient->request(
            'POST',
            $this->userinfoEndpoint,
            $options,
        );
    }
    catch (GuzzleException $exception) {
      throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
    }
  }

  /**
   * {@inheritDoc}
   */
  public function put(array $body) {
    // @todo Implement put() method.
  }

  /**
   * {@inheritDoc}
   */
  public function patch(array $patch) {
    // @todo Implement patch() method.
  }

  /**
   * {@inheritDoc}
   */
  public function delete($resource_id) {
    // @todo Implement delete() method.
  }

  /**
   * {@inheritDoc}
   */
  public function deactivate(array $body) {
    // @todo Implement deactivate() method.
  }

}
