<?php

declare(strict_types=1);

namespace Drupal\cron_fail_alert\EventSubscriber;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Cron Fail Alert event subscriber.
 */
class CronFailAlertSubscriber implements EventSubscriberInterface {

  use StringTranslationTrait;

  /**
   * The cron fail alert settings object.
   */
  protected ImmutableConfig $cronFailAlertSettings;

  /**
   * The site config object.
   */
  protected ImmutableConfig $systemSite;

  /**
   * Constructs event subscriber.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
   *   The language manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\Core\Mail\MailManagerInterface $mailManager
   *   The mail manager service.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state key/value store.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    protected LanguageManagerInterface $languageManager,
    protected LoggerChannelFactoryInterface $loggerFactory,
    protected MailManagerInterface $mailManager,
    protected StateInterface $state,
    protected RequestStack $requestStack,
  ) {
    $this->cronFailAlertSettings = $config_factory->get('cron_fail_alert.settings');
    $this->systemSite = $config_factory->get('system.site');
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      KernelEvents::RESPONSE => ['onKernelResponse'],
    ];
  }

  /**
   * Runs check on the cron job status and sends email alerts if it has failed.
   */
  public function onKernelResponse(): void {
    $frequency = $this->cronFailAlertSettings->get('frequency');
    $lastCheckTimestamp = $this->state->get('cron_fail_alert.last_check_timestamp', 0);

    if ((time() - $lastCheckTimestamp) / 60 < $frequency) {
      return;
    }

    // Update timestamp before the check so concurrent requests don't
    // double-fire.
    $this->state->set('cron_fail_alert.last_check_timestamp', time());
    $this->checkCronStatus();
  }

  /**
   * Compares last cron run time with a set tolerance to check for any failures.
   *
   * @return bool
   *   Returns TRUE if the cron has failed, FALSE otherwise.
   */
  protected function checkCronStatus(): bool {
    // Default to 0 so a site where cron has never run doesn't trigger a
    // false-positive alert immediately after installation.
    $lastCron = $this->state->get('system.cron_last', 0);
    $minutesAgo = (time() - $lastCron) / 60;
    $tolerance = $this->cronFailAlertSettings->get('tolerance');

    if ($minutesAgo < $tolerance) {
      // Cron recovered; clear alert state so the next failure triggers a new
      // alert.
      if ($this->state->get('cron_fail_alert.alerted', FALSE)) {
        $this->state->set('cron_fail_alert.alerted', FALSE);
        $this->loggerFactory->get('cron_fail_alert')->notice('Cron has recovered and is running normally.');
      }
      return FALSE;
    }

    // Suppress repeat alerts until cron recovers; one alert per failure window.
    if ($this->state->get('cron_fail_alert.alerted', FALSE)) {
      return TRUE;
    }

    // Empty string in config means use site default mail.
    $to = $this->cronFailAlertSettings->get('to') ?: $this->systemSite->get('mail');
    $subject = new TranslatableMarkup('@subject on Site: @site_name', [
      '@subject' => $this->cronFailAlertSettings->get('subject'),
      '@site_name' => $this->systemSite->get('name'),
    ]);
    $message = new FormattableMarkup($this->cronFailAlertSettings->get('message'), [
      '@minutes' => floor($minutesAgo),
      ':site' => $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost(),
    ]);

    if ($this->sendCronFailAlertEmail($to, $subject, $message)) {
      $this->state->set('cron_fail_alert.alerted', TRUE);
    }

    return TRUE;
  }

  /**
   * Sends an email notification to the specified recipient.
   *
   * @param string $to
   *   The email address of the recipient.
   * @param \Drupal\Component\Render\MarkupInterface|string $subject
   *   The subject of the email.
   * @param \Drupal\Component\Render\MarkupInterface|string $body
   *   The body of the email.
   *
   * @return bool
   *   TRUE if the email was sent successfully, otherwise FALSE.
   */
  protected function sendCronFailAlertEmail(string $to, MarkupInterface|string $subject, MarkupInterface|string $body): bool {
    $params = [
      'title' => $subject,
      'message' => $body,
    ];
    $langcode = $this->languageManager->getDefaultLanguage()->getId();
    $result = $this->mailManager->mail('cron_fail_alert', 'cron_fail_alert_mail', $to, $langcode, $params);

    if (!$result['result']) {
      $this->loggerFactory->get('cron_fail_alert')->error('Failed to send email notification to @email.', ['@email' => $to]);
      return FALSE;
    }

    $this->loggerFactory->get('cron_fail_alert')->notice('Email notification sent successfully to @email', ['@email' => $to]);
    return TRUE;
  }

}
