<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop_runtime\DTO\Orchestrator\OrchestrationRequest;
use Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorPluginManager;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

/**
 * Service for managing workflow triggers.
 *
 * This service handles:
 * - Processing events and finding matching trigger configurations
 * - Executing workflows via the appropriate orchestrator
 * - Logging trigger execution results.
 *
 * Trigger configurations are stored as FlowDropTriggerConfig entities
 * and queried by event type for efficient matching.
 */
class TriggerManager {

  /**
   * 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_trigger\Service\EntitySerializer $entitySerializer
   *   The entity serializer.
   * @param \Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorPluginManager $orchestratorManager
   *   The orchestrator plugin manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EventTypePluginManager $eventTypeManager,
    protected EntitySerializer $entitySerializer,
    protected OrchestratorPluginManager $orchestratorManager,
    LoggerChannelFactoryInterface $loggerFactory,
    protected EventDispatcherInterface $eventDispatcher,
    protected ConfigFactoryInterface $configFactory,
  ) {
    $this->logger = $loggerFactory->get("flowdrop_trigger");
  }

  /**
   * Process an event and trigger matching workflows.
   *
   * @param string $eventType
   *   The event type (e.g., "entity.insert").
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity involved in the event.
   * @param \Drupal\Core\Entity\EntityInterface|null $original
   *   The original entity (for updates).
   */
  public function processEvent(
    string $eventType,
    EntityInterface $entity,
    ?EntityInterface $original = NULL,
  ): void {
    // Check if this event type is allowed by configuration.
    if (!$this->isEventTypeAllowed($eventType)) {
      // Early exit - event type is not in the allowed list.
      // This helps reduce noise (logging) and improves performance.
      return;
    }

    // For entity-related events, check entity type and bundle restrictions.
    if ($this->isEntityEvent($eventType)) {
      // Check if entity type is allowed.
      if (!$this->isEntityTypeAllowed($entity->getEntityTypeId())) {
        // Early exit - entity type is not in the allowed list.
        return;
      }

      // Check if bundle is allowed.
      if (!$this->isBundleAllowed($entity->getEntityTypeId(), $entity->bundle())) {
        // Early exit - bundle is not in the allowed list.
        return;
      }
    }

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

    $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->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->isEventTypeAllowed($eventType)) {
      // Early exit - event type is not in the allowed list.
      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->executeFormTrigger($config, $eventType, $formId, $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 FlowDropWorkflowInterface) {
        $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;
  }

  /**
   * 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.
   */
  protected function executeTrigger(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    EntityInterface $entity,
    array $entityData,
    ?array $originalData,
  ): void {
    $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;
    }

    $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 GenericEvent($config, [
        "event_type" => $eventType,
        "workflow" => $workflow,
        "entity" => $entity,
      ]),
      "flowdrop_trigger.pre_execute"
    );

    try {
      // Build initial context.
      $initialContext = $this->buildInitialContext(
        $config,
        $eventType,
        $entity,
        $entityData,
        $originalData
      );

      // Get orchestrator from settings.
      $orchestrator = $this->getOrchestrator($config, $eventType);

      // Build orchestration request.
      $orchestratorSettings = $config->getOrchestratorSettings();
      $pipelineId = $this->determinePipelineId($config, $orchestratorSettings);

      $workflowId = $workflow->id();
      if ($workflowId === NULL) {
        $this->logger->error("Workflow has no ID");
        return;
      }

      $request = new OrchestrationRequest(
        workflowId: $workflowId,
        pipelineId: $pipelineId,
        workflow: $this->workflowToArray($workflow),
        initialData: $initialContext,
        options: [
          "trigger_config_id" => $configId,
          "trigger_event" => $eventType,
          "trigger_entity_type" => $entity->getEntityTypeId(),
          "trigger_entity_id" => $entity->id(),
          "priority" => $orchestratorSettings["priority"] ?? "normal",
          "timeout" => $orchestratorSettings["timeout"] ?? 300,
          "retry" => $orchestratorSettings["retry"] ?? [],
        ],
      );

      // 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 GenericEvent($config, [
          "event_type" => $eventType,
          "response" => $response,
        ]),
        "flowdrop_trigger.post_execute"
      );
    }
    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.
   */
  protected function executeFormTrigger(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    string $formId,
    array $triggerData,
    array $context,
  ): void {
    $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;
    }

    $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 GenericEvent($config, [
        "event_type" => $eventType,
        "workflow" => $workflow,
        "form_id" => $formId,
      ]),
      "flowdrop_trigger.pre_execute"
    );

    try {
      // Build initial context from trigger data.
      $initialContext = array_merge($triggerData, [
        "trigger_config_id" => $configId,
        "trigger_node_id" => $config->getNodeId(),
      ]);

      // Get orchestrator from settings.
      $orchestrator = $this->getOrchestrator($config, $eventType);

      // Build orchestration request.
      $orchestratorSettings = $config->getOrchestratorSettings();
      $pipelineId = $this->determinePipelineId($config, $orchestratorSettings);

      $workflowId = $workflow->id();
      if ($workflowId === NULL) {
        $this->logger->error("Workflow has no ID");
        return;
      }

      $request = new OrchestrationRequest(
        workflowId: $workflowId,
        pipelineId: $pipelineId,
        workflow: $this->workflowToArray($workflow),
        initialData: $initialContext,
        options: [
          "trigger_config_id" => $configId,
          "trigger_event" => $eventType,
          "trigger_form_id" => $formId,
          "priority" => $orchestratorSettings["priority"] ?? "normal",
          "timeout" => $orchestratorSettings["timeout"] ?? 300,
          "retry" => $orchestratorSettings["retry"] ?? [],
        ],
      );

      // 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 GenericEvent($config, [
          "event_type" => $eventType,
          "response" => $response,
        ]),
        "flowdrop_trigger.post_execute"
      );
    }
    catch (\Exception $e) {
      $this->logger->error("Form trigger @trigger failed: @message", [
        "@trigger" => $configId,
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Build initial context from trigger config and event data.
   *
   * @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.
   *
   * @return array<string, mixed>
   *   Initial context for workflow execution.
   */
  protected function buildInitialContext(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
    EntityInterface $entity,
    array $entityData,
    ?array $originalData,
  ): array {
    // Default context.
    $context = [
      "event_type" => $eventType,
      "entity_type" => $entity->getEntityTypeId(),
      "entity_id" => $entity->id(),
      "bundle" => $entity->bundle(),
      "entity" => $entityData,
      "original_entity" => $originalData,
      "is_new" => $entity->isNew(),
      "timestamp" => time(),
      "trigger_config_id" => $config->id(),
      "trigger_node_id" => $config->getNodeId(),
    ];

    // Apply data mapping if configured.
    $mapping = $config->getInitialDataMapping();
    if (!empty($mapping)) {
      // Merge mapping with context.
      // @todo Implement JSONPath-based data mapping in the future.
      $context = array_merge($context, $mapping);
    }

    return $context;
  }

  /**
   * Get orchestrator instance from config settings.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param string $eventType
   *   The event type.
   *
   * @return \Drupal\flowdrop_runtime\Service\Orchestrator\OrchestratorInterface
   *   The orchestrator instance.
   */
  protected function getOrchestrator(
    FlowDropTriggerConfigInterface $config,
    string $eventType,
  ): OrchestratorInterface {
    $settings = $config->getOrchestratorSettings();
    $orchestratorType = $settings["type"] ?? "default";

    if ($orchestratorType === "default") {
      // Use event type's default.
      $plugin = $config->getEventTypePlugin();
      $orchestratorType = $plugin !== NULL
        ? $plugin->getDefaultOrchestrator()
        : "asynchronous";
    }

    // Presave events must be synchronous.
    if (str_contains($eventType, ".presave")) {
      $orchestratorType = "synchronous";
    }

    return $this->orchestratorManager->createInstance($orchestratorType);
  }

  /**
   * Determine pipeline ID based on mode.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param array<string, mixed> $settings
   *   Orchestrator settings.
   *
   * @return string
   *   The pipeline ID to use.
   */
  protected function determinePipelineId(
    FlowDropTriggerConfigInterface $config,
    array $settings,
  ): string {
    $mode = $settings["pipeline_mode"] ?? "new";
    $configId = $config->id() ?? "unknown";

    return match ($mode) {
      "reuse" => $settings["pipeline_id"] ?? sprintf("trigger_%s", $configId),
      "singleton" => sprintf("singleton_%s", $configId),
      default => sprintf("trigger_%s_%d_%s", $configId, time(), uniqid()),
    };
  }

  /**
   * Convert workflow entity to array.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return array<string, mixed>
   *   Workflow data as array.
   */
  protected function workflowToArray(FlowDropWorkflowInterface $workflow): array {
    return [
      "id" => $workflow->id(),
      "label" => $workflow->getLabel(),
      "description" => $workflow->getDescription(),
      "nodes" => $workflow->getNodes(),
      "edges" => $workflow->getEdges(),
      "metadata" => $workflow->getMetadata(),
    ];
  }

  /**
   * Get all active triggers (for admin overview).
   *
   * @return array<string, array<array<string, mixed>>>
   *   Triggers grouped by event type.
   */
  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();
      $configId = $config->id();

      if ($configId === NULL) {
        continue;
      }

      $triggers[$eventType][] = [
        "id" => $configId,
        "label" => $config->label(),
        "workflow_id" => $config->getWorkflowId(),
        "node_id" => $config->getNodeId(),
        "conditions" => $config->getConditions(),
        "orchestrator" => $config->getOrchestratorSettings()["type"] ?? "default",
        "weight" => $config->getWeight(),
      ];
    }

    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;
  }

  /**
   * Check if an event type is allowed by configuration.
   *
   * If the allowed_event_types configuration is empty, all event types
   * are allowed. If it's populated, only listed event types are allowed.
   *
   * @param string $eventType
   *   The event type to check.
   *
   * @return bool
   *   TRUE if the event type is allowed, FALSE otherwise.
   */
  protected function isEventTypeAllowed(string $eventType): bool {
    $config = $this->configFactory->get("flowdrop_trigger.settings");
    $allowedEventTypes = $config->get("allowed_event_types") ?? [];

    // If no restrictions are configured, allow all event types.
    if (empty($allowedEventTypes)) {
      return TRUE;
    }

    // Check if the event type is in the allowed list.
    return in_array($eventType, $allowedEventTypes, TRUE);
  }

  /**
   * Check if an event type is an entity-related event.
   *
   * Entity events start with "entity." prefix.
   *
   * @param string $eventType
   *   The event type to check.
   *
   * @return bool
   *   TRUE if this is an entity event, FALSE otherwise.
   */
  protected function isEntityEvent(string $eventType): bool {
    return str_starts_with($eventType, "entity.");
  }

  /**
   * Check if an entity type is allowed by configuration.
   *
   * If the allowed_entity_types configuration is empty, all entity types
   * are allowed. If it's populated, only listed entity types are allowed.
   *
   * @param string $entityType
   *   The entity type to check.
   *
   * @return bool
   *   TRUE if the entity type is allowed, FALSE otherwise.
   */
  protected function isEntityTypeAllowed(string $entityType): bool {
    $config = $this->configFactory->get("flowdrop_trigger.settings");
    $allowedEntityTypes = $config->get("allowed_entity_types") ?? [];

    // If no restrictions are configured, allow all entity types.
    if (empty($allowedEntityTypes)) {
      return TRUE;
    }

    // Check if the entity type is in the allowed list.
    return in_array($entityType, $allowedEntityTypes, TRUE);
  }

  /**
   * Check if a bundle is allowed by configuration.
   *
   * If the allowed_bundles configuration is empty, all bundles are allowed.
   * If it's populated, only listed bundles are allowed.
   * Bundle format is "entity_type:bundle" (e.g., "node:article").
   *
   * @param string $entityType
   *   The entity type.
   * @param string $bundle
   *   The bundle to check.
   *
   * @return bool
   *   TRUE if the bundle is allowed, FALSE otherwise.
   */
  protected function isBundleAllowed(string $entityType, string $bundle): bool {
    $config = $this->configFactory->get("flowdrop_trigger.settings");
    $allowedBundles = $config->get("allowed_bundles") ?? [];

    // If no restrictions are configured, allow all bundles.
    if (empty($allowedBundles)) {
      return TRUE;
    }

    // Build the bundle key in the format "entity_type:bundle".
    $bundleKey = sprintf("%s:%s", $entityType, $bundle);

    // Check if the bundle is in the allowed list.
    return in_array($bundleKey, $allowedBundles, TRUE);
  }

}
