<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop_trigger\Event\TriggerPostExecuteEvent;
use Drupal\flowdrop_trigger\Event\TriggerPreExecuteEvent;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Executes trigger workflows via the orchestration system.
 *
 * Handles entity, form, and cron trigger execution including event dispatching,
 * error handling, and execution state tracking.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class TriggerExecutor {

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

  /**
   * Constructs a TriggerExecutor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\flowdrop_trigger\Service\OrchestrationRequestBuilder $requestBuilder
   *   The orchestration request builder.
   * @param \Drupal\flowdrop_trigger\Service\CronTriggerStateService|null $cronStateService
   *   The cron trigger state service.
   * @param \Drupal\flowdrop_trigger\Service\OverlapPolicyHandler|null $overlapPolicyHandler
   *   The overlap policy handler for cron concurrency control.
   * @param \Drupal\flowdrop_trigger\Service\JitterHandler|null $jitterHandler
   *   The jitter handler for spreading cron trigger execution over a time
   *   window.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly EventDispatcherInterface $eventDispatcher,
    LoggerChannelFactoryInterface $loggerFactory,
    protected readonly OrchestrationRequestBuilder $requestBuilder,
    protected readonly ?CronTriggerStateService $cronStateService = NULL,
    protected readonly ?OverlapPolicyHandler $overlapPolicyHandler = NULL,
    protected readonly ?JitterHandler $jitterHandler = NULL,
  ) {
    $this->logger = $loggerFactory->get("flowdrop_trigger");
  }

  /**
   * Execute a trigger and start workflow execution.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param string $eventType
   *   The event type.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   * @param array<string, mixed> $entityData
   *   Serialized entity data.
   * @param array<string, mixed>|null $originalData
   *   Serialized original entity data.
   */
  public function executeTrigger(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    EntityInterface $entity,
    array $entityData,
    ?array $originalData,
  ): void {
    $workflow = $this->loadWorkflow($config);
    if ($workflow === NULL) {
      return;
    }

    $configId = $config->id();
    if ($configId === NULL) {
      $this->logger->error("Trigger config has no ID");
      return;
    }

    $this->logger->info("Executing trigger @trigger for workflow @workflow", [
      "@trigger" => $configId,
      "@workflow" => $config->getWorkflowId(),
    ]);

    // Dispatch pre-trigger event.
    $this->eventDispatcher->dispatch(
      new TriggerPreExecuteEvent(
        config: $config,
        eventType: $eventType,
        workflow: $workflow,
        entity: $entity,
      ),
      TriggerPreExecuteEvent::NAME
    );

    try {
      $orchestrator = $this->requestBuilder->getOrchestrator($config, $eventType);
      $request = $this->requestBuilder->buildEntityRequest(
        $config, $workflow, $eventType, $entity, $entityData, $originalData
      );

      // Execute.
      $response = $orchestrator->orchestrate($request);

      $this->logger->info("Trigger @trigger executed successfully: @status", [
        "@trigger" => $configId,
        "@status" => $response->getStatus(),
      ]);

      // Dispatch post-trigger event.
      $this->eventDispatcher->dispatch(
        new TriggerPostExecuteEvent(
          config: $config,
          eventType: $eventType,
          response: $response,
        ),
        TriggerPostExecuteEvent::NAME
      );
    }
    catch (\Exception $e) {
      $this->logger->error("Trigger @trigger failed: @message", [
        "@trigger" => $configId,
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Execute a form trigger and start workflow execution.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param string $eventType
   *   The event type.
   * @param string $formId
   *   The form ID.
   * @param array<string, mixed> $triggerData
   *   Extracted trigger data.
   * @param array<string, mixed> $context
   *   Form context.
   */
  public function executeFormTrigger(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    string $formId,
    array $triggerData,
    array $context,
  ): void {
    $workflow = $this->loadWorkflow($config);
    if ($workflow === NULL) {
      return;
    }

    $configId = $config->id();
    if ($configId === NULL) {
      $this->logger->error("Trigger config has no ID");
      return;
    }

    $this->logger->info("Executing form trigger @trigger for workflow @workflow", [
      "@trigger" => $configId,
      "@workflow" => $config->getWorkflowId(),
    ]);

    // Dispatch pre-trigger event.
    $this->eventDispatcher->dispatch(
      new TriggerPreExecuteEvent(
        config: $config,
        eventType: $eventType,
        workflow: $workflow,
        formId: $formId,
      ),
      TriggerPreExecuteEvent::NAME
    );

    try {
      $orchestrator = $this->requestBuilder->getOrchestrator($config, $eventType);
      $request = $this->requestBuilder->buildFormRequest(
        $config, $workflow, $eventType, $formId, $triggerData
      );

      // Execute.
      $response = $orchestrator->orchestrate($request);

      $this->logger->info("Form trigger @trigger executed successfully: @status", [
        "@trigger" => $configId,
        "@status" => $response->getStatus(),
      ]);

      // Dispatch post-trigger event.
      $this->eventDispatcher->dispatch(
        new TriggerPostExecuteEvent(
          config: $config,
          eventType: $eventType,
          response: $response,
        ),
        TriggerPostExecuteEvent::NAME
      );
    }
    catch (\Exception $e) {
      $this->logger->error("Form trigger @trigger failed: @message", [
        "@trigger" => $configId,
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Execute a cron trigger and start workflow execution.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param string $eventType
   *   The event type.
   * @param array<string, mixed> $triggerData
   *   Extracted trigger data.
   * @param array<string, mixed> $context
   *   Cron context.
   */
  public function executeCronTrigger(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    array $triggerData,
    array $context,
  ): void {
    $workflow = $this->loadWorkflow($config);
    if ($workflow === NULL) {
      return;
    }

    $configId = $config->id();
    if ($configId === NULL) {
      $this->logger->error("Trigger config has no ID");
      return;
    }

    // Apply jitter delay before any concurrency or execution logic.
    if ($this->jitterHandler !== NULL) {
      $orchestratorSettings = $config->getOrchestratorSettings();
      $jitter = $orchestratorSettings['jitter'] ?? [];
      if (!empty($jitter['enabled'])) {
        $minSeconds = (int) ($jitter['min_seconds'] ?? 0);
        $maxSeconds = (int) ($jitter['max_seconds'] ?? 0);
        if ($this->jitterHandler->shouldDelay($configId, $minSeconds, $maxSeconds)) {
          return;
        }
      }
    }

    // Apply schedule overlap policy before executing.
    if ($this->overlapPolicyHandler !== NULL) {
      $orchestratorSettings = $config->getOrchestratorSettings();
      $overlapPolicy = $orchestratorSettings['overlap_policy'] ?? '';
      if ($overlapPolicy !== '') {
        [$proceed, $triggerData] = $this->overlapPolicyHandler->resolveExecution(
          $overlapPolicy,
          $config->getWorkflowId(),
          $configId,
          $triggerData,
          $context,
        );
        if (!$proceed) {
          return;
        }
      }
    }

    $startTime = microtime(TRUE);

    $this->logger->info("Executing cron trigger @trigger for workflow @workflow", [
      "@trigger" => $configId,
      "@workflow" => $config->getWorkflowId(),
    ]);

    // Dispatch pre-trigger event.
    $this->eventDispatcher->dispatch(
      new TriggerPreExecuteEvent(
        config: $config,
        eventType: $eventType,
        workflow: $workflow,
      ),
      TriggerPreExecuteEvent::NAME
    );

    try {
      // Get execution state if available.
      $cronState = [];
      if ($this->cronStateService !== NULL) {
        $cronState = $this->cronStateService->getState($configId);
      }

      $orchestrator = $this->requestBuilder->getOrchestrator($config, $eventType);
      $request = $this->requestBuilder->buildCronRequest(
        $config, $workflow, $eventType, $triggerData, $cronState
      );

      // Execute.
      $response = $orchestrator->orchestrate($request);

      // Calculate execution duration.
      $duration = (int) ((microtime(TRUE) - $startTime) * 1000);

      // Record successful execution.
      if ($this->cronStateService !== NULL) {
        $this->cronStateService->recordExecution(
          $configId,
          "success",
          sprintf("Workflow executed: %s", $response->getStatus()),
          $duration
        );
      }

      $this->logger->info("Cron trigger @trigger executed successfully: @status", [
        "@trigger" => $configId,
        "@status" => $response->getStatus(),
      ]);

      // Dispatch post-trigger event.
      $this->eventDispatcher->dispatch(
        new TriggerPostExecuteEvent(
          config: $config,
          eventType: $eventType,
          response: $response,
        ),
        TriggerPostExecuteEvent::NAME
      );

      // Check if this is a one-shot trigger that has reached max executions.
      $this->handleMaxExecutionsReached($config, $configId);
    }
    catch (\Exception $e) {
      // Calculate execution duration even on failure.
      $duration = (int) ((microtime(TRUE) - $startTime) * 1000);

      // Record failed execution.
      if ($this->cronStateService !== NULL) {
        $this->cronStateService->recordExecution(
          $configId,
          "error",
          $e->getMessage(),
          $duration
        );
      }

      $this->logger->error("Cron trigger @trigger failed: @message", [
        "@trigger" => $configId,
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Handle triggers that have reached their max executions limit.
   *
   * Disables one-shot triggers once they've executed the configured
   * number of times.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param string $configId
   *   The trigger configuration ID.
   */
  protected function handleMaxExecutionsReached(
    FlowDropTriggerConfigInterface $config,
    string $configId,
  ): void {
    if ($this->cronStateService === NULL) {
      return;
    }

    $conditions = $config->getConditions();
    $schedule = $conditions["cron_schedule"] ?? [];
    $maxExecutions = (int) ($schedule["max_executions"] ?? 0);

    if ($maxExecutions <= 0) {
      return;
    }

    // Check if max executions reached.
    if ($this->cronStateService->hasReachedMaxExecutions($configId, $maxExecutions)) {
      // Disable the trigger.
      $config->set("status", FALSE);
      $config->save();

      $this->logger->info("Cron trigger @trigger disabled: reached max executions (@max)", [
        "@trigger" => $configId,
        "@max" => $maxExecutions,
      ]);
    }
  }

  /**
   * Load the workflow for a trigger configuration.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   *
   * @return \Drupal\flowdrop_workflow\FlowDropWorkflowInterface|null
   *   The workflow entity, or NULL if not found.
   */
  protected function loadWorkflow(FlowDropTriggerConfigInterface $config): ?FlowDropWorkflowInterface {
    $workflowStorage = $this->entityTypeManager->getStorage("flowdrop_workflow");

    /** @var \Drupal\flowdrop_workflow\FlowDropWorkflowInterface|null $workflow */
    $workflow = $workflowStorage->load($config->getWorkflowId());
    if ($workflow === NULL) {
      $this->logger->error("Workflow not found: @id", [
        "@id" => $config->getWorkflowId(),
      ]);
    }

    return $workflow;
  }

}
