<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Runtime;

use Drupal\flowdrop\DTO\Output;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\DTO\ExecutionContextDTO;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionContext;
use Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionResult;
use Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface;
use Drupal\flowdrop_runtime\Exception\RuntimeException;
use Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface;
use Drupal\flowdrop_runtime\Event\NodeCompletedEvent;
use Drupal\flowdrop_runtime\Service\ParameterResolverInterface;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Core node runtime service with Unified Parameter System support.
 *
 * This service executes nodes using the Unified Parameter System:
 * 1. Loads the node type entity and its parameter configuration
 * 2. Gets the plugin's parameter schema
 * 3. Uses ParameterResolver to resolve parameters from multiple sources
 * 4. Executes the plugin with resolved parameters.
 *
 * All routine logging is handled via ExecutionLoggerInterface, which
 * provides centralized, verbosity-controlled logging.
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see docs/development/unified-parameter-system-spec.md
 * @see \Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface
 */
class NodeRuntimeService implements NodeRuntimeServiceInterface {

  /**
   * Constructs a NodeRuntimeService.
   *
   * @param \Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface $executionLogger
   *   The centralized execution logger.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager $nodeProcessorManager
   *   The node processor plugin manager.
   * @param \Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface $realTimeManager
   *   The real-time manager.
   * @param \Drupal\flowdrop_runtime\Service\ParameterResolverInterface $parameterResolver
   *   The parameter resolver.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(
    private readonly ExecutionLoggerInterface $executionLogger,
    private readonly EventDispatcherInterface $eventDispatcher,
    private readonly FlowDropNodeProcessorPluginManager $nodeProcessorManager,
    private readonly RealTimeManagerInterface $realTimeManager,
    private readonly ParameterResolverInterface $parameterResolver,
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * Execute a single node with real-time updates.
   *
   * This method uses the Unified Parameter System to resolve parameters
   * from runtime inputs, workflow values, and defaults before execution.
   *
   * @param string $executionId
   *   The execution ID.
   * @param string $nodeId
   *   The node instance ID.
   * @param string $nodeType
   *   The node type ID (FlowDropNodeType entity ID).
   * @param array<string, mixed> $runtimeInputs
   *   Runtime inputs from upstream connections.
   * @param array<string, mixed> $workflowValues
   *   Workflow-level configuration values.
   * @param \Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionContext $context
   *   The execution context.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionResult
   *   The execution result.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\RuntimeException
   *   When node execution fails.
   */
  public function executeNode(
    string $executionId,
    string $nodeId,
    string $nodeType,
    array $runtimeInputs,
    array $workflowValues,
    NodeExecutionContext $context,
  ): NodeExecutionResult {
    // Log node start via centralized logger.
    $this->executionLogger->logNodeStart($executionId, $nodeId, $nodeType);

    // Update node status to running.
    $this->realTimeManager->updateNodeStatus($executionId, $nodeId, "running", [
      "node_type" => $nodeType,
      "start_time" => time(),
    ]);

    $startTime = microtime(TRUE);

    try {
      // Load the node type entity.
      $nodeTypeEntity = $this->loadNodeTypeEntity($nodeType);
      $executorPlugin = $nodeTypeEntity->getExecutorPlugin();

      // Get the node processor plugin.
      $processor = $this->nodeProcessorManager->createInstance($executorPlugin);

      // Inject execution context for processors that need it.
      // This provides type-safe access to workflow data, IDs, and metadata.
      if ($processor instanceof ExecutionContextAwareInterface) {
        $executionContext = $this->buildExecutionContext($executionId, $nodeId, $context);
        $processor->setExecutionContext($executionContext);
      }

      // Merge per-node initialData into runtime inputs.
      // If initialData contains an entry keyed by this node's ID, merge it
      // as a fallback — runtime inputs from edges take precedence.
      $allInitialData = $context->getInitialData();
      if (isset($allInitialData[$nodeId]) && is_array($allInitialData[$nodeId])) {
        $runtimeInputs = array_merge($allInitialData[$nodeId], $runtimeInputs);
      }

      // Resolve parameters using the Unified Parameter System.
      $parameterBag = $this->resolveParameters(
        $processor,
        $nodeTypeEntity,
        $runtimeInputs,
        $workflowValues
      );

      // Log parameters at appropriate verbosity level.
      $this->executionLogger->logParameters($executionId, $nodeId, $parameterBag);

      // Execute using the unified parameter execution path.
      try {
        $result = $processor->process($parameterBag);

        // Compose unified "output" port (reserved name) if configured.
        // This provides all exposed outputs as a single JSON object.
        if ($nodeTypeEntity->isUnifiedOutputExposed()) {
          $result["output"] = $this->composeUnifiedOutput($result, $nodeTypeEntity);
        }

        $output = new Output();
        $output->setSuccess();
        // Convert the result to output format.
        $output->fromArray($result);
      }
      catch (\RuntimeException $exception) {
        $output = new Output();
        $output->setError($exception->getMessage());
      }

      $executionTime = microtime(TRUE) - $startTime;

      // Create execution result.
      $result = new NodeExecutionResult(
        nodeId: $nodeId,
        output: $output,
        status: $output->getStatus(),
        executionTime: $executionTime,
        nodeType: $nodeType,
        timestamp: time(),
        context: $context
      );

      // Update node status to complete.
      $this->realTimeManager->updateNodeStatus($executionId, $nodeId, "completed", [
        "execution_time" => $executionTime,
        "output_size" => strlen(serialize($output->toArray())),
        "end_time" => time(),
      ]);

      // Dispatch node execution completed event.
      $event = new NodeCompletedEvent(
        result: $result,
        executionId: $executionId,
        nodeId: $nodeId,
        executionTime: $executionTime,
        timestamp: time(),
      );
      $this->eventDispatcher->dispatch($event, NodeCompletedEvent::NAME);

      // Log completion via centralized logger.
      $this->executionLogger->logNodeComplete($executionId, $result);

      return $result;
    }
    catch (InterruptExceptionInterface $e) {
      // Interrupt exceptions should bubble up to the orchestrator.
      // Update node status to interrupted (not failed).
      $executionTime = microtime(TRUE) - $startTime;
      $this->realTimeManager->updateNodeStatus($executionId, $nodeId, "interrupted", [
        "interrupt_id" => $e->getInterruptId(),
        "interrupt_type" => $e->getInterruptType(),
        "execution_time" => $executionTime,
        "end_time" => time(),
      ]);
      throw $e;
    }
    catch (\Exception $e) {
      $executionTime = microtime(TRUE) - $startTime;

      // Update node status to failed.
      $this->realTimeManager->updateNodeStatus($executionId, $nodeId, "failed", [
        "error" => $e->getMessage(),
        "execution_time" => $executionTime,
        "end_time" => time(),
      ]);

      // Log failure via centralized logger.
      $this->executionLogger->logNodeFailure($executionId, $nodeId, $e, $executionTime);

      throw new RuntimeException(
        "Node execution failed for {$nodeId}: " . $e->getMessage(),
        0,
        $e
      );
    }
  }

  /**
   * Resolve parameters using the Unified Parameter System.
   *
   * @param object $processor
   *   The node processor plugin instance.
   * @param \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface $nodeTypeEntity
   *   The node type entity with parameter configuration.
   * @param array<string, mixed> $runtimeInputs
   *   Runtime inputs from upstream connections.
   * @param array<string, mixed> $workflowValues
   *   Workflow-level configuration values.
   *
   * @return \Drupal\flowdrop\DTO\ParameterBagInterface
   *   The resolved parameters.
   */
  protected function resolveParameters(
    object $processor,
    FlowDropNodeTypeInterface $nodeTypeEntity,
    array $runtimeInputs,
    array $workflowValues,
  ): ParameterBagInterface {
    // Get the plugin's parameter schema.
    $parameterSchema = $processor->getParameterSchema();

    // Get the entity's parameter configuration (overrides).
    $entityParameters = $nodeTypeEntity->getParameters();

    // Use ParameterResolver to resolve values from all sources.
    return $this->parameterResolver->resolve(
      $parameterSchema,
      $entityParameters,
      $workflowValues,
      $runtimeInputs
    );
  }

  /**
   * Load a node type entity.
   *
   * @param string $nodeType
   *   The node type ID.
   *
   * @return \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface
   *   The node type entity.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\RuntimeException
   *   When node type is not found.
   */
  protected function loadNodeTypeEntity(string $nodeType): FlowDropNodeTypeInterface {
    $entity = $this->entityTypeManager
      ->getStorage("flowdrop_node_type")
      ->load($nodeType);

    if (!$entity instanceof FlowDropNodeTypeInterface) {
      throw new RuntimeException("Node type '{$nodeType}' not found");
    }

    return $entity;
  }

  /**
   * Execute multiple nodes in sequence with real-time updates.
   *
   * @param string $executionId
   *   The execution ID.
   * @param array<array<string, mixed>> $nodes
   *   Array of node definitions with 'id', 'type', 'inputs', and 'config'.
   * @param \Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionContext $context
   *   The execution context.
   *
   * @return array<string, \Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionResult>
   *   Array of execution results keyed by node ID.
   */
  public function executeNodes(
    string $executionId,
    array $nodes,
    NodeExecutionContext $context,
  ): array {
    $results = [];

    foreach ($nodes as $node) {
      $result = $this->executeNode(
        $executionId,
        $node["id"],
        $node["type"],
        $node["inputs"] ?? [],
        $node["config"] ?? [],
        $context
      );

      $results[$node["id"]] = $result;

      // Update context with node output for next nodes.
      $context->addNodeOutput($node["id"], $result->getOutput());
    }

    return $results;
  }

  /**
   * Validate node before execution.
   *
   * @param string $executionId
   *   The execution ID for logging.
   * @param string $nodeId
   *   The node instance ID for logging.
   * @param string $nodeType
   *   The node type ID.
   * @param array<string, mixed> $params
   *   The parameters to validate.
   *
   * @return \Drupal\flowdrop\DTO\ValidationResult
   *   The validation result.
   */
  public function validateNode(
    string $executionId,
    string $nodeId,
    string $nodeType,
    array $params,
  ): ValidationResult {
    try {
      $nodeTypeEntity = $this->loadNodeTypeEntity($nodeType);
      $processor = $this->nodeProcessorManager->createInstance(
        $nodeTypeEntity->getExecutorPlugin()
      );
      $result = $processor->validateParams($params);

      // Log validation result via centralized logger.
      $this->executionLogger->logValidation($executionId, $nodeId, $result);

      return $result;
    }
    catch (\Exception $e) {
      // Return validation failure for load errors.
      return ValidationResult::error(
        "node_type",
        "Failed to load node type '{$nodeType}': " . $e->getMessage(),
        "NODE_TYPE_LOAD_ERROR"
      );
    }
  }

  /**
   * Get node processor information.
   *
   * @param string $nodeType
   *   The node type ID.
   *
   * @return array<string, mixed>
   *   Node processor information.
   */
  public function getNodeProcessorInfo(string $nodeType): array {
    try {
      $nodeTypeEntity = $this->loadNodeTypeEntity($nodeType);
      $processor = $this->nodeProcessorManager->createInstance(
        $nodeTypeEntity->getExecutorPlugin()
      );

      return [
        "id" => $processor->getPluginId(),
        "name" => $processor->getName(),
        "description" => $processor->getDescription(),
        "category" => $nodeTypeEntity->getCategory(),
        "tags" => $nodeTypeEntity->getTags(),
        "version" => $processor->getVersion(),
        "parameter_schema" => $processor->getParameterSchema(),
        "output_schema" => $processor->getOutputSchema(),
      ];
    }
    catch (\Exception $e) {
      return [];
    }
  }

  /**
   * Builds an ExecutionContextDTO from NodeExecutionContext.
   *
   * Context is now always passed via NodeExecutionContext, which is
   * injected into processors implementing ExecutionContextAwareInterface.
   *
   * @param string $executionId
   *   The execution ID.
   * @param string $nodeId
   *   The node instance ID.
   * @param \Drupal\flowdrop_runtime\DTO\Runtime\NodeExecutionContext $context
   *   The node execution context.
   *
   * @return \Drupal\flowdrop\DTO\ExecutionContextDTO
   *   The execution context DTO.
   */
  protected function buildExecutionContext(
    string $executionId,
    string $nodeId,
    NodeExecutionContext $context,
  ): ExecutionContextDTO {
    return new ExecutionContextDTO(
      initialData: $context->getInitialData(),
      workflowId: $context->getWorkflowId(),
      pipelineId: $context->getPipelineId(),
      executionId: $executionId,
      nodeId: $nodeId,
      metadata: $context->getMetadata(),
    );
  }

  /**
   * Get the execution logger.
   *
   * Allows external code to access the logger for workflow-level logging.
   *
   * @return \Drupal\flowdrop_runtime\Service\Logging\ExecutionLoggerInterface
   *   The execution logger.
   */
  public function getExecutionLogger(): ExecutionLoggerInterface {
    return $this->executionLogger;
  }

  /**
   * Compose unified "output" port (reserved name) from exposed outputs only.
   *
   * This creates a single JSON object containing all outputs that are
   * configured as exposed (exposed: true) in the node type entity.
   * Internal outputs (starting with _) and trigger are excluded.
   *
   * @param array<string, mixed> $outputData
   *   Raw output data from the plugin's process() method.
   * @param \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface $nodeTypeEntity
   *   The node type entity with output configuration.
   *
   * @return array<string, mixed>
   *   Filtered output containing only exposed keys.
   */
  protected function composeUnifiedOutput(
    array $outputData,
    FlowDropNodeTypeInterface $nodeTypeEntity,
  ): array {
    $entityOutputs = $nodeTypeEntity->getOutputs();
    $unified = [];

    foreach ($outputData as $key => $value) {
      // Skip internal keys (starting with _ or __).
      if (str_starts_with($key, "_")) {
        continue;
      }

      // Skip trigger - it has its own port.
      if ($key === "trigger") {
        continue;
      }

      // Check if exposed (default to TRUE if not configured).
      $outputConfig = $entityOutputs[$key] ?? [];
      $exposed = $outputConfig["exposed"] ?? TRUE;

      if ($exposed) {
        $unified[$key] = $value;
      }
    }

    return $unified;
  }

}
