<?php

namespace Drupal\keycloak_user_provisioning\Helper;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\user_provisioning\Helpers\moUserProvisioningLogger;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Drupal\Core\Messenger\MessengerInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Helper class for Keycloak integration operations.
 */
class MoKeycloakHelper {
  use StringTranslationTrait;
  /**
   * ImmutableConfig property.
   *
   * @var Drupal\Core\Config\ImmutableConfig
   */
  private ImmutableConfig $config;

  /**
   * Config property.
   *
   * @var Drupal\Core\Config\Config
   */
  private Config $configFactory;

  /**
   * Messenger property.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Base URL of the site.
   *
   * @var string
   */
  private $baseUrl;

  /**
   * Logger property.
   *
   * @var Drupal\user_provisioning\Helpers\moUserProvisioningLogger
   */
  private moUserProvisioningLogger $moLogger;

  /**
   * The httpclient property.
   *
   * @var GuzzleHttp\Client
   */
  private Client $httpClient;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected ConfigFactoryInterface $configFactoryService;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected ModuleHandlerInterface $moduleHandler;

  /**
   * Constructs a new MoKeycloakHelper object.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \GuzzleHttp\Client $http_client
   *   The HTTP client.
   */
  public function __construct(
    RequestStack $request_stack,
    ConfigFactoryInterface $config_factory,
    MessengerInterface $messenger,
    ModuleHandlerInterface $module_handler,
    Client $http_client,
  ) {
    $base_url = $request_stack->getCurrentRequest()->getSchemeAndHttpHost();
    $this->baseUrl = $base_url;
    $this->configFactoryService = $config_factory;
    $this->config = $config_factory->get('keycloak_user_provisioning.settings');
    $this->configFactory = $config_factory->getEditable('keycloak_user_provisioning.settings');
    $this->messenger = $messenger;
    $this->moduleHandler = $module_handler;
    if ($module_handler->moduleExists('user_provisioning')) {
      $this->moLogger = new moUserProvisioningLogger();
    }
    $this->httpClient = $http_client;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('request_stack'),
      $container->get('config.factory'),
      $container->get('messenger'),
      $container->get('module_handler'),
      $container->get('http_client')
    );
  }

  /**
   * Fetches user attributes from Keycloak and saves them into configuration.
   *
   * @throws \Exception
   * @throws \GuzzleHttp\Exception\GuzzleException
   */
  public function fetchAttributes() {
    $access_token = $this->getKeyCloakAccessToken();

    // Step 2: Handle missing token gracefully.
    if (empty($access_token)) {
      $this->messenger->addMessage(
            $this->t('Unable to fetch attributes. Failed to obtain access token from Keycloak. Please verify your Keycloak configuration settings and try again.'),
            MessengerInterface::TYPE_ERROR
        );
    }

    $token_data = json_decode($access_token, TRUE);
    // Check if access_token contains an error JSON or a valid token string.
    if (is_array($token_data) && isset($token_data['error'])) {
      // Access token response contains an error.
      $content = $token_data;
    }
    else {
      // Access token is valid; fetch users from Keycloak.
      $response = self::getUsersFromKeycloak($access_token);

      // Decode response safely (if it’s JSON)
      $decoded_response = json_decode($response, TRUE);
      $content = is_array($decoded_response) ? $decoded_response : [];
    }

    $user_details = $this->moKeycloakArrayFlattenAttributes(is_array($content) && !isset($content['error']) ? $content[0] : $content);

    $user_details_encoded = Json::encode($user_details);

    $this->configFactory
      ->set('mo_keycloak_attr_list_from_server', $user_details_encoded)
      ->save();
  }

  /**
   * Retrieves users from Keycloak using the provided access token.
   *
   * @param string $access_token
   *   The access token for authentication.
   *
   * @return string
   *   The response body containing user data.
   */
  public function getUsersFromKeycloak($access_token) {
    $url = $this->getUsersEndPointUrl();
    $options = [
      'timeout' => 30,
      'headers' => [
        'Authorization' => 'Bearer ' . $access_token,
        'Content-Type' => 'application/json',
      ],
    ];

    try {
      $response = $this->httpClient->request('GET', $url, $options);
    }
    catch (GuzzleException $exception) {
      if ($exception->getCode() == 0) {
        $error_msg = $exception->getMessage();
        $error_code = [
          "%error" => $error_msg,
          "%Description" => "Please Configure the fields correctly.",
        ];

        $content = strtr('Error: %error Cause: %Description', $error_code);

        $this->moLogger->addLog($content, __LINE__, __FUNCTION__, __FILE__);

        $this->messenger->addError($this->t('An error occurred: @error', [
          '@error' => $error_msg,
        ]));
        $this->configFactory
          ->set('mo_keycloak_attr_list_from_server', '')
          ->save();
        $response = new RedirectResponse(Url::fromRoute('keycloak_user_provisioning.overview', [], ['query' => ['tab' => 'drupal-to-keycloak-configuration']])->toString());
        $response->send();
        exit();
      }
      else {
        $this->configFactory
          ->set('mo_keycloak_attr_list_from_server', '')
          ->save();
        $error = [
          '%error' => $exception->getResponse()->getBody()->getContents(),
        ];
        $content = strtr('Error: %error Cause: %Description', $error);
        $this->moLogger->addLog($content, __LINE__, __FUNCTION__, __FILE__);
        foreach ($error as $value) {
          return $value;
        }
      }
    }

    $content = $response->getBody()->getContents();
    return $content;
  }

  /**
   * Flattens user attributes received from Keycloak.
   *
   * @param array $details
   *   The user info array.
   *
   * @return array
   *   Returns flattened array
   */
  public function moKeycloakArrayFlattenAttributes($details): array {
    $arr = [];
    foreach ($details as $key => $value) {

      if (empty($value)) {
        continue;
      }
      if (!is_object($value)&& !is_array($value)) {
        $arr[$key] = filter_var($value);
      }
      else {
        $this->moKeycloakArrayFlattenAttributesLvl2($key, $value, $arr);
      }
    }

    return $arr;
  }

  /**
   * Seperates user attributes by | if nested.
   *
   * @param string $index
   *   Key of array.
   * @param array $arr
   *   The array.
   * @param array $haystack
   *   The array of flattened attributes.
   */
  private function moKeycloakArrayFlattenAttributesLvl2($index, $arr, &$haystack): void {
    foreach ($arr as $key => $value) {
      if (empty($value)) {
        continue;
      }

      if (!is_object($value) && !is_array($value)) {

        if (!strpos(strtolower($index), 'error')) {
          $haystack[$index . "|" . $key] = $value;
        }

      }
      else {
        $this->moKeycloakArrayFlattenAttributesLvl2($index . "|" . $key, $value, $haystack);
      }
    }
  }

  /**
   * Gets the Keycloak users endpoint URL.
   *
   * @return string
   *   The users endpoint URL.
   */
  public function getUsersEndPointUrl() {
    $keycloak_realm = $this->config->get('keycloak_user_provisioning_realm');
    $keycloak_domain = $this->config->get('keycloak_user_provisioning_base_url');
    return rtrim($keycloak_domain, '/') . '/admin/realms/' . $keycloak_realm . '/users';
  }

  /**
   * Retrieves access token from Keycloak.
   *
   * @return string|null
   *   The access token or NULL on failure.
   *
   * @throws \GuzzleHttp\Exception\GuzzleException
   * @throws \Exception
   */
  public function getKeyCloakAccessToken() {
    $keycloak_client_id = $this->config->get('keycloak_user_provisioning_client_id');
    $keycloak_client_secret = $this->config->get('keycloak_user_provisioning_client_secret');
    $keycloak_realm = $this->config->get('keycloak_user_provisioning_realm');
    $keycloak_domain = $this->config->get('keycloak_user_provisioning_base_url');

    $token_url = rtrim($keycloak_domain, '/') .
            '/realms/' . $keycloak_realm .
            '/protocol/openid-connect/token';

    try {
      $response = $this->httpClient->request('POST', $token_url, [
        'timeout' => 30,
        'headers' => [
          'Content-Type' => 'application/x-www-form-urlencoded',
        ],
        'form_params' => [
          'grant_type' => 'client_credentials',
          'client_id' => $keycloak_client_id,
          'client_secret' => $keycloak_client_secret,
        ],
      ]);

      $status_code = $response->getStatusCode();
      if ($status_code !== 200) {
        $this->messenger->addMessage("Failed to fetch access token. HTTP status: {$status_code}", MessengerInterface::TYPE_ERROR);
        return NULL;
      }

      $body = (string) $response->getBody();
      $token = json_decode($body, TRUE);

      if (json_last_error() !== JSON_ERROR_NONE) {
        $this->messenger->addMessage('Invalid JSON response from Keycloak: ' . json_last_error_msg(), MessengerInterface::TYPE_ERROR);
        return NULL;
      }

      if (empty($token['access_token'])) {
        $this->messenger->addMessage('Access token not found in Keycloak response.', MessengerInterface::TYPE_ERROR);
        return NULL;
      }

      return $token['access_token'];

    }
    catch (RequestException $e) {
      $this->configFactory
        ->set('mo_keycloak_attr_list_from_server', '')
        ->save();

      $message = $e->getMessage();

      if (preg_match('/(\{.*\})/s', $message, $matches)) {
        // Found a JSON error in the message.
        $error_message = $matches[1];
        json_decode($error_message, TRUE);

        if (json_last_error() !== JSON_ERROR_NONE) {
          $error_message = '{"error":"unexpected_response","user_message":"We received an unexpected response from the server. Please try again later."}';
        }
      }
      else {
        // No JSON found — detect common error types.
        if (str_contains($message, '401')) {
          $error_message = '{"error":"Authentication failed. Please verify your Client ID and Client Secret."}';
        }
        elseif (str_contains($message, '404')) {
          $error_message = '{"error":"Requested resource not found. Please check your Keycloak Domain or Realm settings."}';
        }
        else {
          $error_message = '{"error":"Something went wrong while connecting to the server. Please try again later."}';
        }
      }

      return $error_message;
    }
    catch (\Exception $e) {
      $this->messenger->addMessage('Error while fetching Keycloak access token: ' . $e->getMessage(), MessengerInterface::TYPE_ERROR);
      return NULL;
    }
  }

}
