<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Compiler;

use Psr\Log\LoggerInterface;
use Drupal\flowdrop_runtime\DTO\Compiler\CompiledWorkflow;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph;
use Drupal\flowdrop_runtime\DTO\Compiler\ExcludedNode;
use Drupal\flowdrop_runtime\DTO\Compiler\ExecutionGraph;
use Drupal\flowdrop_runtime\DTO\Compiler\ExecutionNode;
use Drupal\flowdrop_runtime\DTO\Compiler\NodeMapping;
use Drupal\flowdrop_workflow\DTO\WorkflowDTO;
use Drupal\flowdrop_runtime\Exception\CompilationException;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * Compiles workflow entities into executable DAGs.
 *
 * Produces two graphs:
 * - DependencyGraph: Complete picture of all node connections
 * - ExecutionGraph: Only nodes that should be executed by the orchestrator.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class WorkflowCompiler implements WorkflowCompilerInterface {

  /**
   * Logger channel for this service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private readonly LoggerInterface $logger;

  /**
   * Constructs the WorkflowCompiler.
   *
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger channel factory.
   */
  public function __construct(LoggerChannelFactoryInterface $loggerFactory) {
    $this->logger = $loggerFactory->get("flowdrop_runtime");
  }

  /**
   * Compiles a workflow entity into dependency and execution graphs.
   *
   * @param array<string, mixed>|\Drupal\flowdrop_workflow\DTO\WorkflowDTO $workflow
   *   The workflow definition array or WorkflowDTO.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\CompiledWorkflow
   *   The compiled workflow with both graphs.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\CompilationException
   *   When compilation fails due to validation errors.
   */
  public function compile(array|WorkflowDTO $workflow): CompiledWorkflow {
    // Convert array to WorkflowDTO if needed.
    if (is_array($workflow)) {
      $workflowDTO = WorkflowDTO::fromArray($workflow);
    }
    else {
      $workflowDTO = $workflow;
    }

    $this->logger->info("Compiling workflow @id", [
      "@id" => $workflowDTO->getId(),
    ]);

    try {
      // Validate workflow structure.
      $this->validateWorkflowStructure($workflowDTO);

      // Build dependency graph (ALL edges).
      $dependencyGraph = DependencyGraph::fromWorkflow($workflowDTO);

      // Detect circular dependencies (excluding special edges).
      $this->detectCircularDependencies($dependencyGraph);

      // Build execution graph (execution flow only).
      $executionGraph = $this->buildExecutionGraph($workflowDTO, $dependencyGraph);

      // Create node mappings.
      $nodeMappings = $this->createNodeMappings($workflowDTO);

      $compiledWorkflow = new CompiledWorkflow(
        workflowId: $workflowDTO->getId(),
        dependencyGraph: $dependencyGraph,
        executionGraph: $executionGraph,
        nodeMappings: $nodeMappings,
        metadata: [
          "totalNodeCount" => $dependencyGraph->getNodeCount(),
          "executableNodeCount" => $executionGraph->getNodeCount(),
          "excludedNodeCount" => $executionGraph->getExcludedCount(),
          "edgeCount" => $dependencyGraph->getEdgeCount(),
          "compilationTimestamp" => time(),
        ]
      );

      $this->logger->info("Successfully compiled workflow @id: @exec executable, @excluded excluded", [
        "@id" => $workflowDTO->getId(),
        "@exec" => $executionGraph->getNodeCount(),
        "@excluded" => $executionGraph->getExcludedCount(),
      ]);

      return $compiledWorkflow;

    }
    catch (\Exception $e) {
      $this->logger->error("Workflow compilation failed: @error", [
        "@error" => $e->getMessage(),
        "@workflow_id" => $workflowDTO->getId(),
      ]);

      throw new CompilationException(
        "Workflow compilation failed: " . $e->getMessage(),
        0,
        $e
      );
    }
  }

  /**
   * Validates workflow structure using DTO.
   *
   * @param \Drupal\flowdrop_workflow\DTO\WorkflowDTO $workflow
   *   The workflow DTO.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\CompilationException
   *   When workflow structure is invalid.
   */
  private function validateWorkflowStructure(WorkflowDTO $workflow): void {
    if (empty($workflow->getId())) {
      throw new CompilationException("Workflow must have an ID");
    }

    if ($workflow->getNodeCount() === 0) {
      throw new CompilationException("Workflow must have at least one node");
    }

    // Validate each node has required properties.
    foreach ($workflow->getNodes() as $node) {
      if (empty($node->getId())) {
        throw new CompilationException("All nodes must have an ID");
      }

      if (empty($node->getTypeId())) {
        throw new CompilationException("Node {$node->getId()} must have a type");
      }
    }
  }

  /**
   * Detects circular dependencies using DFS.
   *
   * Only considers edges that affect execution order (data and trigger),
   * not special edges like tool_availability which are intentionally cyclic.
   *
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $dependencyGraph
   *   The dependency graph.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\CompilationException
   *   When circular dependencies are detected.
   */
  private function detectCircularDependencies(DependencyGraph $dependencyGraph): void {
    // Build a simplified graph with only execution-relevant edges.
    $executionEdges = [];
    foreach ($dependencyGraph->getAllNodes() as $nodeId => $node) {
      $executionEdges[$nodeId] = [];
    }

    foreach ($dependencyGraph->getAllEdges() as $edge) {
      // Only include edges that affect execution order.
      if (!$edge->isExcludedFromExecution()) {
        $executionEdges[$edge->getSource()][] = $edge->getTarget();
      }
    }

    $visited = [];
    $recursionStack = [];

    foreach (array_keys($executionEdges) as $nodeId) {
      if (!isset($visited[$nodeId])) {
        if ($this->hasCycle($nodeId, $executionEdges, $visited, $recursionStack)) {
          throw new CompilationException(
            "Circular dependency detected in workflow"
          );
        }
      }
    }
  }

  /**
   * Checks for cycles using DFS.
   *
   * @param string $nodeId
   *   The current node ID.
   * @param array<string, array<string>> $graph
   *   The adjacency list graph.
   * @param array<string, bool> $visited
   *   Visited nodes.
   * @param array<string, bool> $recursionStack
   *   Current recursion stack.
   *
   * @return bool
   *   TRUE if cycle is detected.
   */
  private function hasCycle(
    string $nodeId,
    array $graph,
    array &$visited,
    array &$recursionStack,
  ): bool {
    $visited[$nodeId] = TRUE;
    $recursionStack[$nodeId] = TRUE;

    foreach ($graph[$nodeId] as $dependentId) {
      if (!isset($visited[$dependentId])) {
        if ($this->hasCycle($dependentId, $graph, $visited, $recursionStack)) {
          return TRUE;
        }
      }
      elseif (isset($recursionStack[$dependentId]) && $recursionStack[$dependentId]) {
        return TRUE;
      }
    }

    $recursionStack[$nodeId] = FALSE;
    return FALSE;
  }

  /**
   * Builds the execution graph based on execution rules.
   *
   * Rules (in priority order):
   * 1. Nodes with ONLY
   *    tool_availability/loopback/agent_result
   *    inputs are excluded
   * 2. Nodes with no edges (orphans) are excluded
   * 3. Trigger edges determine execution order when present
   * 4. Data edges determine order if no triggers.
   *
   * @param \Drupal\flowdrop_workflow\DTO\WorkflowDTO $workflow
   *   The workflow DTO.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $dependencyGraph
   *   The dependency graph.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\ExecutionGraph
   *   The execution graph.
   */
  private function buildExecutionGraph(
    WorkflowDTO $workflow,
    DependencyGraph $dependencyGraph,
  ): ExecutionGraph {
    $executableNodes = [];
    $excludedNodes = [];

    // First pass: identify which nodes should be executed vs excluded.
    foreach ($dependencyGraph->getAllNodes() as $nodeId => $depNode) {
      $exclusionReason = $this->shouldExcludeNode($nodeId, $dependencyGraph);

      if ($exclusionReason !== NULL) {
        $excludedNodes[$nodeId] = $exclusionReason;
        $this->logger->debug("Excluding node @id: @reason", [
          "@id" => $nodeId,
          "@reason" => $exclusionReason->getReason(),
        ]);
      }
      else {
        $executableNodes[$nodeId] = $depNode;
      }
    }

    // Second pass: build execution dependencies for executable nodes.
    $executionNodeData = [];
    $inDegrees = [];

    foreach ($executableNodes as $nodeId => $depNode) {
      // Get trigger dependencies (highest priority).
      $triggerDeps = $this->getTriggerDependencies($nodeId, $dependencyGraph, $executableNodes);

      // Get data dependencies (for data resolution, not execution order).
      $dataDeps = $this->getDataDependencies($nodeId, $dependencyGraph);

      // Determine execution dependencies:
      // - If trigger edges exist, they control execution order
      // - Otherwise, data edges control execution order.
      $executionDeps = !empty($triggerDeps) ? $triggerDeps : $this->filterExecutableDeps($dataDeps, $executableNodes);

      $executionNodeData[$nodeId] = [
        "depNode" => $depNode,
        "triggerDeps" => $triggerDeps,
        "dataDeps" => $dataDeps,
        "executionDeps" => $executionDeps,
      ];

      $inDegrees[$nodeId] = count($executionDeps);
    }

    // Topological sort for execution order.
    $executionOrder = $this->topologicalSort($executionNodeData, $inDegrees);

    // Build dependents map (who depends on whom).
    $dependentsMap = $this->buildDependentsMap($executionNodeData);

    // Third pass: create ExecutionNode objects.
    $nodes = [];
    foreach ($executionNodeData as $nodeId => $data) {
      $depNode = $data["depNode"];

      // Determine if root (no execution dependencies).
      $isRoot = empty($data["executionDeps"]);

      // Determine if leaf (no dependents in execution graph).
      $isLeaf = empty($dependentsMap[$nodeId] ?? []);

      // Determine special type.
      $specialType = $depNode->getSpecialType();

      $nodes[$nodeId] = new ExecutionNode(
        id: $nodeId,
        typeId: $depNode->getTypeId(),
        config: $depNode->getConfig(),
        triggerDependencies: $data["triggerDeps"],
        dataDependencies: $data["dataDeps"],
        isRoot: $isRoot,
        isLeaf: $isLeaf,
        specialType: $specialType,
        metadata: [
          "label" => $depNode->getLabel(),
          "executionDependencies" => $data["executionDeps"],
        ]
      );
    }

    return new ExecutionGraph(
      nodes: $nodes,
      executionOrder: $executionOrder,
      excludedNodes: $excludedNodes,
      metadata: [
        "rootCount" => count(array_filter($nodes, fn($n) => $n->isRoot())),
        "leafCount" => count(array_filter($nodes, fn($n) => $n->isLeaf())),
        "agentCount" => count(array_filter($nodes, fn($n) => $n->isAgent())),
        "iteratorCount" => count(array_filter($nodes, fn($n) => $n->isIterator())),
      ]
    );
  }

  /**
   * Determines if a node should be excluded from execution.
   *
   * @param string $nodeId
   *   The node ID.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $dependencyGraph
   *   The dependency graph.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\ExcludedNode|null
   *   ExcludedNode if should be excluded, NULL otherwise.
   */
  private function shouldExcludeNode(
    string $nodeId,
    DependencyGraph $dependencyGraph,
  ): ?ExcludedNode {
    $incomingEdges = $dependencyGraph->getIncomingEdges($nodeId);
    $outgoingEdges = $dependencyGraph->getOutgoingEdges($nodeId);

    // Check if orphan (no edges at all).
    if (empty($incomingEdges) && empty($outgoingEdges)) {
      return ExcludedNode::forNoEdges($nodeId);
    }

    // Check if only connected via tool_availability as INPUT.
    // A node is excluded if ALL its incoming edges are tool_availability.
    if (!empty($incomingEdges)) {
      $hasNonToolInput = FALSE;
      $toolParentNode = NULL;

      foreach ($incomingEdges as $edge) {
        if ($edge->isToolAvailability()) {
          $toolParentNode = $edge->getSource();
        }
        else {
          $hasNonToolInput = TRUE;
          break;
        }
      }

      if (!$hasNonToolInput && $toolParentNode !== NULL) {
        return ExcludedNode::forToolOnly($nodeId, $toolParentNode);
      }
    }

    // Root nodes (no incoming edges) should be included.
    if (empty($incomingEdges)) {
      return NULL;
    }

    // Check if only connected via loopback as INPUT.
    $hasNonLoopbackInput = FALSE;
    $loopbackParent = NULL;
    foreach ($incomingEdges as $edge) {
      if ($edge->isLoopback()) {
        $loopbackParent = $edge->getSource();
      }
      else {
        $hasNonLoopbackInput = TRUE;
        break;
      }
    }

    if (!$hasNonLoopbackInput && $loopbackParent !== NULL) {
      return ExcludedNode::forLoopbackOnly($nodeId, $loopbackParent);
    }

    return NULL;
  }

  /**
   * Gets trigger dependencies for a node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $dependencyGraph
   *   The dependency graph.
   * @param array<string, mixed> $executableNodes
   *   Map of executable node IDs.
   *
   * @return array<string>
   *   Array of trigger dependency node IDs.
   */
  private function getTriggerDependencies(
    string $nodeId,
    DependencyGraph $dependencyGraph,
    array $executableNodes,
  ): array {
    $deps = [];
    $triggerEdges = $dependencyGraph->getIncomingEdges($nodeId, DependencyEdge::TYPE_TRIGGER);

    foreach ($triggerEdges as $edge) {
      $sourceId = $edge->getSource();
      // Only include if source is also executable.
      if (isset($executableNodes[$sourceId])) {
        $deps[] = $sourceId;
      }
    }

    return array_unique($deps);
  }

  /**
   * Gets data dependencies for a node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $dependencyGraph
   *   The dependency graph.
   *
   * @return array<string>
   *   Array of data dependency node IDs.
   */
  private function getDataDependencies(
    string $nodeId,
    DependencyGraph $dependencyGraph,
  ): array {
    $deps = [];
    $dataEdges = $dependencyGraph->getIncomingEdges($nodeId, DependencyEdge::TYPE_DATA);

    foreach ($dataEdges as $edge) {
      $deps[] = $edge->getSource();
    }

    return array_unique($deps);
  }

  /**
   * Filters dependencies to only include executable nodes.
   *
   * @param array<string> $deps
   *   Array of dependency node IDs.
   * @param array<string, mixed> $executableNodes
   *   Map of executable node IDs.
   *
   * @return array<string>
   *   Filtered array of executable dependency node IDs.
   */
  private function filterExecutableDeps(array $deps, array $executableNodes): array {
    return array_values(array_filter($deps, fn($id) => isset($executableNodes[$id])));
  }

  /**
   * Performs topological sort on execution nodes.
   *
   * @param array<string, array<string, mixed>> $nodeData
   *   Node data with execution dependencies.
   * @param array<string, int> $inDegrees
   *   In-degree counts for each node.
   *
   * @return array<string>
   *   Topologically sorted node IDs.
   */
  private function topologicalSort(array $nodeData, array $inDegrees): array {
    $queue = [];
    $result = [];

    // Find all nodes with no dependencies (in_degree = 0).
    foreach ($inDegrees as $nodeId => $degree) {
      if ($degree === 0) {
        $queue[] = $nodeId;
      }
    }

    // Build adjacency list for dependents.
    $dependents = [];
    foreach ($nodeData as $nodeId => $data) {
      foreach ($data["executionDeps"] as $depId) {
        $dependents[$depId][] = $nodeId;
      }
    }

    // Process queue.
    while (!empty($queue)) {
      $nodeId = array_shift($queue);
      $result[] = $nodeId;

      // Reduce in-degree for all dependents.
      foreach ($dependents[$nodeId] ?? [] as $dependentId) {
        $inDegrees[$dependentId]--;
        if ($inDegrees[$dependentId] === 0) {
          $queue[] = $dependentId;
        }
      }
    }

    return $result;
  }

  /**
   * Builds a map of which nodes depend on which other nodes.
   *
   * @param array<string, array<string, mixed>> $nodeData
   *   Node data with execution dependencies.
   *
   * @return array<string, array<string>>
   *   Map of node ID to array of dependent node IDs.
   */
  private function buildDependentsMap(array $nodeData): array {
    $dependents = [];
    foreach (array_keys($nodeData) as $nodeId) {
      $dependents[$nodeId] = [];
    }

    foreach ($nodeData as $nodeId => $data) {
      foreach ($data["executionDeps"] as $depId) {
        if (isset($dependents[$depId])) {
          $dependents[$depId][] = $nodeId;
        }
      }
    }

    return $dependents;
  }

  /**
   * Creates node mappings for processor plugins.
   *
   * @param \Drupal\flowdrop_workflow\DTO\WorkflowDTO $workflow
   *   The workflow DTO.
   *
   * @return array<string, NodeMapping>
   *   Array of NodeMapping objects keyed by node ID.
   */
  private function createNodeMappings(WorkflowDTO $workflow): array {
    $nodeMappings = [];

    foreach ($workflow->getNodes() as $nodeId => $node) {
      $metadata = [];
      if (method_exists($node, "getMetadata")) {
        $metadata = $node->getMetadata();
      }

      $inputs = [];
      if (method_exists($node, "getInputs")) {
        $inputs = $node->getInputs();
      }

      $outputs = [];
      if (method_exists($node, "getOutputs")) {
        $outputs = $node->getOutputs();
      }

      $nodeMappings[$nodeId] = new NodeMapping(
        nodeId: $nodeId,
        processorId: $node->getTypeId(),
        config: $node->getConfig(),
        metadata: [
          "label" => $node->getLabel(),
          "description" => $metadata["description"] ?? "",
          "category" => $metadata["category"] ?? "",
          "inputs" => $inputs,
          "outputs" => $outputs,
        ]
      );
    }

    return $nodeMappings;
  }

  /**
   * Validates a compiled workflow.
   *
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\CompiledWorkflow $compiledWorkflow
   *   The compiled workflow to validate.
   *
   * @return bool
   *   TRUE if valid.
   */
  public function validateCompiledWorkflow(CompiledWorkflow $compiledWorkflow): bool {
    $executionGraph = $compiledWorkflow->getExecutionGraph();
    $nodeMappings = $compiledWorkflow->getNodeMappings();

    // Check that all nodes in execution order have mappings.
    foreach ($executionGraph->getExecutionOrder() as $nodeId) {
      if (!isset($nodeMappings[$nodeId])) {
        $this->logger->error("Execution graph references unmapped node: @node_id", [
          "@node_id" => $nodeId,
        ]);
        return FALSE;
      }
    }

    return TRUE;
  }

}
