<?php

namespace Drupal\rest_api_authentication\Controller;

use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\user\UserAuthInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * Controller for the rest api authentication module.
 */
class RestApiAuthenticationController extends ControllerBase {
  /**
   * The form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * The Request Stack Service.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

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

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $moMessenger;

  /**
   * The user auth service.
   *
   * @var \Drupal\user\UserAuthInterface
   */
  protected $moUserAuth;

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $moEntityTypeManager;

  /**
   * The module logger channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $moLogger;

  /**
   * The flood service.
   *
   * @var \Drupal\Core\Flood\FloodInterface
   */
  protected $moFlood;

  /**
   * The CSRF token generator service.
   *
   * @var \Drupal\Core\Access\CsrfTokenGenerator
   */
  protected CsrfTokenGenerator $moCsrfToken;

  /**
   * Constructor.
   */
  public function __construct(FormBuilderInterface $form_builder, RequestStack $request_stack, ConfigFactoryInterface $config_factory, MessengerInterface $messenger, UserAuthInterface $user_auth, EntityTypeManagerInterface $entity_type_manager, LoggerChannelFactoryInterface $logger_factory, FloodInterface $flood, CsrfTokenGenerator $csrf_token) {
    $this->formBuilder = $form_builder;
    $this->requestStack = $request_stack;
    $this->moConfigFactory = $config_factory;
    $this->moMessenger = $messenger;
    $this->moUserAuth = $user_auth;
    $this->moEntityTypeManager = $entity_type_manager;
    $this->moLogger = $logger_factory->get('rest_api_authentication');
    $this->moFlood = $flood;
    $this->moCsrfToken = $csrf_token;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('form_builder'),
      $container->get('request_stack'),
      $container->get('config.factory'),
      $container->get('messenger'),
      $container->get('user.auth'),
      $container->get('entity_type.manager'),
      $container->get('logger.factory'),
      $container->get('flood'),
      $container->get('csrf_token')
    );
  }

  /**
   * Opens the support request form in a modal dialog.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function openSupportRequestForm() {
    $response = new AjaxResponse();
    $modal_form = $this->formBuilder->getForm('\Drupal\rest_api_authentication\Form\MiniornageAPIAuthnRequestSupport');
    $request = $this->requestStack->getCurrentRequest();
    if ($request->isXmlHttpRequest()) {
      $response->addCommand(new OpenModalDialogCommand($this->t('Support Request/Contact Us'), $modal_form, ['width' => '40%']));
    }
    else {
      $response = $modal_form;
    }
    return $response;
  }

  /**
   * Opens the trial request form in a modal dialog.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function openTrialRequestForm() {
    $response = new AjaxResponse();
    $modal_form = $this->formBuilder->getForm('\Drupal\rest_api_authentication\Form\MiniornageAPIAuthnRequestTrial');
    $request = $this->requestStack->getCurrentRequest();

    if ($request->isXmlHttpRequest()) {
      $response->addCommand(new OpenModalDialogCommand('Request 7-Days Full Feature Trial License', $modal_form, ['width' => '40%']));
    }
    else {
      $response = $modal_form;
    }
    return $response;
  }

  /**
   * Deletes an application by ID.
   */
  public function deleteApplication($app_id) {
    $token = $this->requestStack->getCurrentRequest()->query->get('token', '');
    if (!$this->moCsrfToken->validate($token, 'rest_api_auth_delete_' . $app_id)) {
      throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
    }

    $config = $this->moConfigFactory->getEditable('rest_api_authentication.settings');
    $applications = $config->get('applications');

    if (is_string($applications)) {
      $applications = json_decode($applications, TRUE) ?: [];
    }
    elseif (!is_array($applications)) {
      $applications = [];
    }

    if (isset($applications[$app_id])) {
      unset($applications[$app_id]);

      $config->set('application_name', '')
        ->set('authentication_method', NULL)
        ->set('applications', $applications)
        ->save();

      $this->moMessenger->addMessage($this->t('Application deleted successfully.'));
    }
    else {
      $this->moMessenger->addError($this->t('Application not found.'));
    }

    return new RedirectResponse('/admin/config/people/rest_api_authentication/auth_settings?tab=edit-api-auth');
  }

  /**
   * Sets an application as the default application.
   *
   * @param string $app_id
   *   The application ID to set as default.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function setDefaultApplication($app_id) {
    $token = $this->requestStack->getCurrentRequest()->query->get('token', '');
    if (!$this->moCsrfToken->validate($token, 'rest_api_auth_set_default_' . $app_id)) {
      throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
    }

    $config = $this->moConfigFactory->getEditable('rest_api_authentication.settings');
    $applications = $config->get('applications');

    if (is_string($applications)) {
      $applications = json_decode($applications, TRUE) ?: [];
    }
    elseif (!is_array($applications)) {
      $applications = [];
    }

    if (isset($applications[$app_id])) {
      foreach ($applications as $key => $app) {
        $applications[$key]['is_default'] = FALSE;
      }

      $applications[$app_id]['is_default'] = TRUE;

      $config->set('applications', $applications)
        ->set('default_application_id', $app_id)
        ->save();

      $this->moMessenger->addMessage($this->t('Application set as default.'));
    }
    else {
      $this->moMessenger->addError($this->t('Application not found.'));
    }

    return new RedirectResponse('/admin/config/people/rest_api_authentication/auth_settings?tab=edit-api-auth');
  }

  /**
   * Revokes an access token.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The JSON response.
   */
  public function revokeToken(Request $request): JsonResponse {
    // Rate-limit by client IP: max 5 failed credential attempts per 5 minutes.
    $client_ip = $request->getClientIp();
    $flood_event = 'rest_api_authentication.revoke';
    if (!$this->moFlood->isAllowed($flood_event, 5, 300, $client_ip)) {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 429,
        'error' => 'RATE_LIMITED',
        'error_description' => 'Too many failed attempts. Please try again later.',
      ], 429);
    }

    $authorization_header = $request->headers->get('Authorization') ?: $request->headers->get('Authorisation');

    if (empty($authorization_header) || stripos($authorization_header, 'Basic ') !== 0) {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 401,
        'error' => 'MISSING_BASIC_AUTH',
        'error_description' => 'Basic Authentication is required for this endpoint.',
      ], 401);
    }

    $encoded_credentials = trim(substr($authorization_header, 6));
    $decoded_credentials = base64_decode($encoded_credentials, TRUE);

    if ($decoded_credentials === FALSE || !str_contains($decoded_credentials, ':')) {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 401,
        'error' => 'INVALID_BASIC_AUTH_FORMAT',
        'error_description' => 'Invalid Basic Authentication format.',
      ], 401);
    }

    $credentials = explode(':', $decoded_credentials, 2);
    $username = $credentials[0];
    $password = $credentials[1];

    if (empty($username) || empty($password)) {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 401,
        'error' => 'MISSING_CREDENTIALS',
        'error_description' => 'Username and password are required.',
      ], 401);
    }

    if (!$this->moUserAuth->authenticate($username, $password)) {
      // Register a flood event on credential failure to throttle brute-force.
      $this->moFlood->register($flood_event, 300, $client_ip);
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 401,
        'error' => 'INVALID_CREDENTIALS',
        'error_description' => 'Invalid username or password.',
      ], 401);
    }

    // Credentials verified — clear flood counter for this IP.
    $this->moFlood->clear($flood_event, $client_ip);

    try {
      $users = $this->moEntityTypeManager->getStorage('user')->loadByProperties(['name' => $username]);
    }
    catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
      $this->moLogger->error('Basic Auth :@error', ['@error' => $e->getMessage()]);
    }

    $user = !empty($users) ? reset($users) : NULL;
    if ($user->isBlocked()) {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 403,
        'error' => 'USER_BLOCKED',
        'error_description' => 'The user account is blocked.',
      ], 403);
    }

    if ($request->getMethod() !== 'POST') {
      return new JsonResponse([
        'status' => 'error',
        'http_code' => 405,
        'error' => 'INVALID_HTTP_METHOD',
        'error_description' => 'The used HTTP method should be a POST.',
      ], 405);
    }

    return new JsonResponse([
      'status' => 'error',
      'http_code' => 403,
      'error' => 'PREMIUM_FEATURE',
      'error_description' => 'Token revocation is a premium feature. Please upgrade to premium to use this functionality.',
    ], 403);
  }

}
