<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop\Service\EntitySerializer;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Drupal\user\UserInterface;
use Psr\Log\LoggerInterface;

/**
 * Service for managing workflow triggers.
 *
 * This service handles:
 * - Processing events and finding matching trigger configurations
 * - Delegating execution to the TriggerExecutor
 * - Logging trigger execution results.
 *
 * Trigger configurations are stored as FlowDropTriggerConfig entities
 * and queried by event type for efficient matching.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class TriggerManager implements TriggerManagerInterface {

  use DependencySerializationTrait;

  /**
   * Logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * Constructs a TriggerManager.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\flowdrop_trigger\Service\EventTypePluginManager $eventTypeManager
   *   The event type plugin manager.
   * @param \Drupal\flowdrop\Service\EntitySerializer $entitySerializer
   *   The entity serializer.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\flowdrop_trigger\Service\EventFilterService $eventFilter
   *   The event filter service.
   * @param \Drupal\flowdrop_trigger\Service\TriggerExecutor $triggerExecutor
   *   The trigger executor service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\flowdrop_trigger\Service\CronScheduleMatcher|null $cronScheduleMatcher
   *   The cron schedule matcher service.
   * @param \Drupal\flowdrop_trigger\Service\CronTriggerStateService|null $cronStateService
   *   The cron trigger state service.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EventTypePluginManager $eventTypeManager,
    protected EntitySerializer $entitySerializer,
    LoggerChannelFactoryInterface $loggerFactory,
    protected EventFilterService $eventFilter,
    protected TriggerExecutor $triggerExecutor,
    protected ConfigFactoryInterface $configFactory,
    protected ?CronScheduleMatcher $cronScheduleMatcher = NULL,
    protected ?CronTriggerStateService $cronStateService = NULL,
  ) {
    $this->logger = $loggerFactory->get("flowdrop_trigger");
  }

  /**
   * Process an event and trigger matching workflows.
   *
   * For update/presave events, the original entity is automatically retrieved
   * from $entity->original if available.
   *
   * @param string $eventType
   *   The event type (e.g., "entity.insert").
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity involved in the event.
   * @param array $context
   *   Optional context data for the event.
   */
  public function processEvent(
    string $eventType,
    EntityInterface $entity,
    array $context = [],
  ): void {
    // Check if this event type is allowed by configuration.
    if (!$this->eventFilter->isEventTypeAllowed($eventType)) {
      return;
    }

    // For entity-related events, check entity type and bundle restrictions.
    if ($this->eventFilter->isEntityEvent($eventType)) {
      if (!$this->eventFilter->isEntityTypeAllowed($entity->getEntityTypeId())) {
        return;
      }
      if (!$this->eventFilter->isBundleAllowed($entity->getEntityTypeId(), $entity->bundle())) {
        return;
      }
    }

    // For user-related events, check role restrictions.
    if ($this->eventFilter->isUserEvent($eventType) && $entity instanceof UserInterface) {
      $roles = $entity->getRoles();
      if (!$this->eventFilter->isUserRoleAllowed($roles)) {
        return;
      }
    }

    $this->logger->debug("Processing trigger event @event for @type @id", [
      "@event" => $eventType,
      "@type" => $entity->getEntityTypeId(),
      "@id" => $entity->id() ?? "new",
    ]);

    // Get original entity if available (set by Drupal during presave/update).
    $original = $entity instanceof ContentEntityInterface
      ? $entity->getOriginal()
      : NULL;

    $context = array_merge($context, [
      "entity_type" => $entity->getEntityTypeId(),
      "bundle" => $entity->bundle(),
      "entity" => $entity,
      "original" => $original,
    ]);

    // Query trigger configs by event type.
    $matchingConfigs = $this->loadMatchingTriggers($eventType, $context);

    if (empty($matchingConfigs)) {
      $this->logger->debug("No matching triggers found for @event", [
        "@event" => $eventType,
      ]);
      return;
    }

    $this->logger->info("Found @count matching triggers for @event on @type/@bundle", [
      "@count" => count($matchingConfigs),
      "@event" => $eventType,
      "@type" => $entity->getEntityTypeId(),
      "@bundle" => $entity->bundle(),
    ]);

    // Serialize entity data once for all triggers.
    $entityData = $this->entitySerializer->serialize($entity);
    $originalData = $original !== NULL
      ? $this->entitySerializer->serialize($original)
      : NULL;

    // Execute each matching trigger.
    foreach ($matchingConfigs as $config) {
      $this->triggerExecutor->executeTrigger($config, $eventType, $entity, $entityData, $originalData);
    }
  }

  /**
   * Process a form event and trigger matching workflows.
   *
   * @param string $eventType
   *   The event type (e.g., "form.submit", "form.validate").
   * @param string $formId
   *   The form ID.
   * @param array<string, mixed> $context
   *   Form context data (form, form_values, form_state, etc.).
   */
  public function processFormEvent(
    string $eventType,
    string $formId,
    array $context = [],
  ): void {
    // Check if this event type is allowed by configuration.
    if (!$this->eventFilter->isEventTypeAllowed($eventType)) {
      return;
    }

    // Check if form ID is allowed by configuration.
    if (!$this->eventFilter->isFormIdAllowed($formId)) {
      return;
    }

    $this->logger->debug("Processing form trigger event @event for form @form_id", [
      "@event" => $eventType,
      "@form_id" => $formId,
    ]);

    // Add form_id to context.
    $context["form_id"] = $formId;

    // Query trigger configs by event type.
    $matchingConfigs = $this->loadMatchingTriggers($eventType, $context);

    if (empty($matchingConfigs)) {
      $this->logger->debug("No matching triggers found for @event on form @form_id", [
        "@event" => $eventType,
        "@form_id" => $formId,
      ]);
      return;
    }

    $this->logger->info("Found @count matching triggers for @event on form @form_id", [
      "@count" => count($matchingConfigs),
      "@event" => $eventType,
      "@form_id" => $formId,
    ]);

    // Get event type plugin to extract trigger data.
    try {
      $eventTypePlugin = $this->eventTypeManager->createInstance($eventType);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to load event type plugin @event: @message", [
        "@event" => $eventType,
        "@message" => $e->getMessage(),
      ]);
      return;
    }

    // Extract trigger data using event type plugin.
    $triggerData = $eventTypePlugin->extractTriggerData(
      $context["entity"] ?? NULL,
      NULL,
      $context
    );

    // Execute each matching trigger.
    foreach ($matchingConfigs as $config) {
      $this->triggerExecutor->executeFormTrigger($config, $eventType, $formId, $triggerData, $context);
    }
  }

  /**
   * Process a cron event and trigger matching workflows.
   *
   * @param string $eventType
   *   The event type (e.g., "cron.run").
   * @param array<string, mixed> $context
   *   Cron context data (cron_key, last_run, etc.).
   */
  public function processCronEvent(
    string $eventType,
    array $context = [],
  ): void {
    // Check if this event type is allowed by configuration.
    if (!$this->eventFilter->isEventTypeAllowed($eventType)) {
      return;
    }

    // Check cron-specific settings.
    $maxWorkflows = $this->getMaxWorkflowsPerCronRun();

    $this->logger->debug("Processing cron trigger event @event", [
      "@event" => $eventType,
    ]);

    // Query trigger configs by event type.
    $matchingConfigs = $this->loadMatchingTriggers($eventType, $context);

    if (empty($matchingConfigs)) {
      $this->logger->debug("No matching triggers found for @event", [
        "@event" => $eventType,
      ]);
      return;
    }

    // Filter by per-trigger schedules if schedule matcher is available.
    if ($this->cronScheduleMatcher !== NULL) {
      $scheduledConfigs = [];
      foreach ($matchingConfigs as $config) {
        if ($this->cronScheduleMatcher->shouldTriggerRun($config)) {
          $scheduledConfigs[] = $config;
        }
        else {
          // Optionally record skipped execution for debugging.
          $configId = $config->id();
          if ($configId !== NULL && $this->cronStateService !== NULL) {
            $this->cronStateService->recordSkipped($configId, "Schedule conditions not met");
          }
        }
      }
      $matchingConfigs = $scheduledConfigs;
    }

    if (empty($matchingConfigs)) {
      $this->logger->debug("No triggers scheduled to run for @event", [
        "@event" => $eventType,
      ]);
      return;
    }

    // Apply max workflows limit if configured.
    if ($maxWorkflows > 0 && count($matchingConfigs) > $maxWorkflows) {
      $matchingConfigs = array_slice($matchingConfigs, 0, $maxWorkflows);
      $this->logger->info("Limiting cron triggers to @max workflows", [
        "@max" => $maxWorkflows,
      ]);
    }

    $this->logger->info("Found @count scheduled triggers for cron event @event", [
      "@count" => count($matchingConfigs),
      "@event" => $eventType,
    ]);

    // Get event type plugin to extract trigger data.
    try {
      $eventTypePlugin = $this->eventTypeManager->createInstance($eventType);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to load event type plugin @event: @message", [
        "@event" => $eventType,
        "@message" => $e->getMessage(),
      ]);
      return;
    }

    // Extract trigger data using event type plugin.
    $triggerData = $eventTypePlugin->extractTriggerData(
      NULL,
      NULL,
      $context
    );

    // Execute each matching trigger.
    foreach ($matchingConfigs as $config) {
      $this->triggerExecutor->executeCronTrigger($config, $eventType, $triggerData, $context);
    }
  }

  /**
   * Load trigger configs matching event criteria.
   *
   * @param string $eventType
   *   The event type.
   * @param array<string, mixed> $context
   *   Event context.
   *
   * @return array<\Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface>
   *   Matching trigger configurations, sorted by weight.
   */
  protected function loadMatchingTriggers(string $eventType, array $context): array {
    $configStorage = $this->entityTypeManager->getStorage("flowdrop_trigger_config");
    $workflowStorage = $this->entityTypeManager->getStorage("flowdrop_workflow");

    // Query by event_type and status.
    /** @var \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface[] $configs */
    $configs = $configStorage->loadByProperties([
      "event_type" => $eventType,
      "status" => TRUE,
    ]);

    $matchingConfigs = [];

    foreach ($configs as $config) {
      $workflowId = $config->getWorkflowId();

      // Verify workflow exists and is enabled.
      $workflow = $workflowStorage->load($workflowId);
      if (!$workflow instanceof WorkflowDefinitionInterface) {
        $this->logger->warning(
          "Trigger @trigger references non-existent workflow @workflow",
          ["@trigger" => $config->id(), "@workflow" => $workflowId]
        );
        continue;
      }

      if (!$workflow->status()) {
        // Workflow disabled - skip silently.
        continue;
      }

      // Check conditions match.
      if (!$config->matches($eventType, $context)) {
        continue;
      }

      $matchingConfigs[] = $config;
    }

    // Sort by weight.
    usort($matchingConfigs, fn($a, $b) => $a->getWeight() <=> $b->getWeight());

    return $matchingConfigs;
  }

  /**
   * Get all active triggers (for admin overview).
   *
   * @return array<string, list<\Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface>>
   *   Triggers grouped by event type. Keys are event type strings,
   *   values are lists of trigger config entities.
   */
  public function getAllActiveTriggers(): array {
    $storage = $this->entityTypeManager->getStorage("flowdrop_trigger_config");

    /** @var \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface[] $configs */
    $configs = $storage->loadByProperties(["status" => TRUE]);

    $triggers = [];

    foreach ($configs as $config) {
      $eventType = $config->getEventType();
      $triggers[$eventType][] = $config;
    }

    return $triggers;
  }

  /**
   * Get triggers for a specific workflow.
   *
   * @param string $workflowId
   *   The workflow ID.
   *
   * @return array<\Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface>
   *   Trigger configurations for the workflow.
   */
  public function getTriggersForWorkflow(string $workflowId): array {
    $storage = $this->entityTypeManager->getStorage("flowdrop_trigger_config");

    /** @var \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface[] $configs */
    $configs = $storage->loadByProperties(["workflow_id" => $workflowId]);

    return $configs;
  }

  /**
   * Get max workflows per cron run from config.
   *
   * @return int
   *   The max workflows per cron run (0 = unlimited).
   */
  protected function getMaxWorkflowsPerCronRun(): int {
    $config = $this->configFactory->get("flowdrop_trigger.settings.cron");
    return (int) ($config->get("max_workflows_per_run") ?? 0);
  }

}
