<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\flowdrop\Service\JsonPath\JsonPathServiceInterface;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;

/**
 * Builds orchestration requests from trigger configurations.
 *
 * Handles context building, orchestrator selection, pipeline ID generation,
 * and orchestrator-specific options mapping.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class OrchestrationRequestBuilder {

  /**
   * Constructs an OrchestrationRequestBuilder.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager $orchestratorManager
   *   The orchestrator plugin manager.
   * @param \Drupal\flowdrop\Service\JsonPath\JsonPathServiceInterface|null $jsonPathService
   *   The JSONPath service for data mapping.
   */
  public function __construct(
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly OrchestratorPluginManager $orchestratorManager,
    protected readonly ?JsonPathServiceInterface $jsonPathService = NULL,
  ) {}

  /**
   * 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.
   */
  public 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.
    // Mappings can use JSONPath expressions (starting with "$") or
    // property paths.
    // Example mappings:
    // - "user_email" => "$.entity.email" (JSONPath expression)
    // - "user_name" => "entity.name" (Property path, legacy)
    // - "active_tags" => "$.entity.tags[?(@.active)]" (JSONPath filter)
    // - "static_value" => "literal:my_value" (Literal value)
    $mapping = $config->getInitialDataMapping();
    if (!empty($mapping) && $this->jsonPathService !== NULL) {
      $mappedData = $this->jsonPathService->applyMappings($context, $mapping);
      $context = array_merge($context, $mappedData);
    }
    elseif (!empty($mapping)) {
      // Fallback: merge mapping directly (legacy behavior).
      $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_orchestration\Service\Orchestrator\OrchestratorInterface
   *   The orchestrator instance.
   */
  public 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();
      if ($plugin !== NULL) {
        $orchestratorType = $plugin->getDefaultOrchestrator();
      }
      else {
        // Fallback to general settings default orchestrator.
        $generalSettings = $this->configFactory->get("flowdrop_trigger.settings");
        $orchestratorType = $generalSettings->get("default_orchestrator") ?? "flowdrop_runtime:asynchronous";
      }
    }

    // Presave events must be synchronous (basic synchronous, not
    // pipeline-based).
    // This ensures presave events complete during the entity save process.
    if (str_contains($eventType, ".presave")) {
      $orchestratorType = "flowdrop_runtime:synchronous";
    }

    // Validate orchestrator type exists.
    if (!$this->orchestratorManager->hasDefinition($orchestratorType)) {
      $orchestratorType = "flowdrop_runtime:asynchronous";
    }

    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.
   */
  public 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()),
    };
  }

  /**
   * Build orchestration options from trigger orchestrator settings.
   *
   * Creates a complete options array for OrchestrationRequest that includes
   * all orchestrator-specific settings. This ensures that each orchestrator
   * type receives its specific configuration (e.g., stategraph settings,
   * pipeline settings, queue settings, etc.).
   *
   * @param array<string, mixed> $orchestratorSettings
   *   The orchestrator settings from the trigger configuration.
   * @param string $configId
   *   The trigger configuration ID.
   * @param string $eventType
   *   The event type that triggered the execution.
   * @param string|null $entityType
   *   The entity type (for entity triggers).
   * @param string|int|null $entityId
   *   The entity ID (for entity triggers).
   * @param string|null $formId
   *   The form ID (for form triggers).
   *
   * @return array<string, mixed>
   *   The complete options array for OrchestrationRequest.
   */
  public function buildOrchestrationOptions(
    array $orchestratorSettings,
    string $configId,
    string $eventType,
    ?string $entityType = NULL,
    string|int|null $entityId = NULL,
    ?string $formId = NULL,
  ): array {
    // Start with trigger metadata that all orchestrators might need.
    $options = [
      "trigger_config_id" => $configId,
      "trigger_event" => $eventType,
    ];

    // Add entity-specific metadata if present.
    if ($entityType !== NULL) {
      $options["trigger_entity_type"] = $entityType;
    }
    if ($entityId !== NULL) {
      $options["trigger_entity_id"] = $entityId;
    }

    // Add form-specific metadata if present.
    if ($formId !== NULL) {
      $options["trigger_form_id"] = $formId;
    }

    // Add common settings that most orchestrators use.
    $options["priority"] = $orchestratorSettings["priority"] ?? "normal";
    $options["timeout"] = $orchestratorSettings["timeout"] ?? 300;

    // Add retry settings (used by pipeline-based orchestrators).
    if (isset($orchestratorSettings["retry"])) {
      $options["retry"] = $orchestratorSettings["retry"];
    }

    // Add pipeline-specific settings.
    if (isset($orchestratorSettings["pipeline_mode"])) {
      $options["pipeline_mode"] = $orchestratorSettings["pipeline_mode"];
    }
    if (isset($orchestratorSettings["pipeline_id"])) {
      $options["pipeline_id"] = $orchestratorSettings["pipeline_id"];
    }
    if (isset($orchestratorSettings["max_concurrent_jobs"])) {
      $options["max_concurrent_jobs"] = $orchestratorSettings["max_concurrent_jobs"];
    }
    if (isset($orchestratorSettings["job_priority_strategy"])) {
      $options["job_priority_strategy"] = $orchestratorSettings["job_priority_strategy"];
    }
    if (isset($orchestratorSettings["retry_strategy"])) {
      $options["retry_strategy"] = $orchestratorSettings["retry_strategy"];
    }

    // Add StateGraph-specific settings.
    // The stategraph orchestrator expects these in options["stategraph"].
    if (isset($orchestratorSettings["stategraph"])) {
      // Convert from the trigger config format to the ExecutionConfig format.
      $stategraphSettings = $orchestratorSettings["stategraph"];
      $options["stategraph"] = [
        "checkpointerType" => $stategraphSettings["checkpointer_type"] ?? "memory",
        "maxIterations" => $stategraphSettings["max_iterations"] ?? 100,
        "createCheckpoints" => $stategraphSettings["create_checkpoints"] ?? TRUE,
        "debugMode" => $stategraphSettings["debug_mode"] ?? FALSE,
        "threadId" => $stategraphSettings["thread_id"] ?? NULL,
        "resumeFromCheckpoint" => $stategraphSettings["resume_from_checkpoint"] ?? NULL,
      ];
    }

    // Add synchronous orchestrator settings.
    if (isset($orchestratorSettings["enable_realtime_updates"])) {
      $options["enable_realtime_updates"] = $orchestratorSettings["enable_realtime_updates"];
    }
    if (isset($orchestratorSettings["log_level"])) {
      $options["log_level"] = $orchestratorSettings["log_level"];
    }

    // Add asynchronous orchestrator settings.
    if (isset($orchestratorSettings["queue_name"])) {
      $options["queue_name"] = $orchestratorSettings["queue_name"];
    }
    if (isset($orchestratorSettings["batch_size"])) {
      $options["batch_size"] = $orchestratorSettings["batch_size"];
    }

    return $options;
  }

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

  /**
   * Build a complete OrchestrationRequest for an entity trigger.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   * @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 \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest
   *   The orchestration request.
   */
  public function buildEntityRequest(
    FlowDropTriggerConfigInterface $config,
    FlowDropWorkflowInterface $workflow,
    string $eventType,
    EntityInterface $entity,
    array $entityData,
    ?array $originalData,
  ): OrchestrationRequest {
    $configId = (string) $config->id();
    $workflowId = (string) $workflow->id();
    $orchestratorSettings = $config->getOrchestratorSettings();

    $initialContext = $this->buildInitialContext($config, $eventType, $entity, $entityData, $originalData);
    $pipelineId = $this->determinePipelineId($config, $orchestratorSettings);
    $options = $this->buildOrchestrationOptions(
      $orchestratorSettings,
      $configId,
      $eventType,
      $entity->getEntityTypeId(),
      $entity->id()
    );

    return new OrchestrationRequest(
      workflowId: $workflowId,
      pipelineId: $pipelineId,
      workflow: $this->workflowToArray($workflow),
      initialData: $initialContext,
      options: $options,
    );
  }

  /**
   * Build a complete OrchestrationRequest for a form trigger.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   * @param string $eventType
   *   The event type.
   * @param string $formId
   *   The form ID.
   * @param array<string, mixed> $triggerData
   *   Extracted trigger data.
   *
   * @return \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest
   *   The orchestration request.
   */
  public function buildFormRequest(
    FlowDropTriggerConfigInterface $config,
    FlowDropWorkflowInterface $workflow,
    string $eventType,
    string $formId,
    array $triggerData,
  ): OrchestrationRequest {
    $configId = (string) $config->id();
    $workflowId = (string) $workflow->id();
    $orchestratorSettings = $config->getOrchestratorSettings();

    $initialContext = array_merge($triggerData, [
      "trigger_config_id" => $configId,
      "trigger_node_id" => $config->getNodeId(),
    ]);

    $pipelineId = $this->determinePipelineId($config, $orchestratorSettings);
    $options = $this->buildOrchestrationOptions(
      $orchestratorSettings,
      $configId,
      $eventType,
      NULL,
      NULL,
      $formId
    );

    return new OrchestrationRequest(
      workflowId: $workflowId,
      pipelineId: $pipelineId,
      workflow: $this->workflowToArray($workflow),
      initialData: $initialContext,
      options: $options,
    );
  }

  /**
   * Build a complete OrchestrationRequest for a cron trigger.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $config
   *   The trigger configuration.
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   * @param string $eventType
   *   The event type.
   * @param array<string, mixed> $triggerData
   *   Extracted trigger data.
   * @param array<string, mixed> $cronState
   *   Cron execution state (run_count, last_run).
   *
   * @return \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest
   *   The orchestration request.
   */
  public function buildCronRequest(
    FlowDropTriggerConfigInterface $config,
    FlowDropWorkflowInterface $workflow,
    string $eventType,
    array $triggerData,
    array $cronState = [],
  ): OrchestrationRequest {
    $configId = (string) $config->id();
    $workflowId = (string) $workflow->id();
    $orchestratorSettings = $config->getOrchestratorSettings();

    $initialContext = array_merge($triggerData, [
      "trigger_config_id" => $configId,
      "trigger_node_id" => $config->getNodeId(),
    ]);

    // Add execution state info if available.
    if (!empty($cronState)) {
      $initialContext["cron_execution_count"] = ($cronState["run_count"] ?? 0) + 1;
      $initialContext["cron_last_run"] = $cronState["last_run"] ?? NULL;
    }

    $pipelineId = $this->determinePipelineId($config, $orchestratorSettings);
    $options = $this->buildOrchestrationOptions(
      $orchestratorSettings,
      $configId,
      $eventType,
    );

    return new OrchestrationRequest(
      workflowId: $workflowId,
      pipelineId: $pipelineId,
      workflow: $this->workflowToArray($workflow),
      initialData: $initialContext,
      options: $options,
    );
  }

}
