<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\DTO\Compiler;

use Drupal\flowdrop\Utility\EdgePortClassifier;

/**
 * Represents an edge in the dependency graph.
 *
 * Contains all metadata about a connection between two nodes,
 * including edge type, handles, and additional metadata.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class DependencyEdge {

  /**
   * Edge type constants.
   */
  public const TYPE_DATA = "data";
  public const TYPE_TRIGGER = "trigger";
  public const TYPE_TOOL_AVAILABILITY = "tool_availability";
  public const TYPE_LOOPBACK = "loopback";
  public const TYPE_AGENT_RESULT = "agent_result";

  /**
   * Constructs a new DependencyEdge.
   *
   * @param string $id
   *   The unique edge identifier.
   * @param string $source
   *   The source node ID.
   * @param string $target
   *   The target node ID.
   * @param string $sourceHandle
   *   The source handle (port identifier).
   * @param string $targetHandle
   *   The target handle (port identifier).
   * @param string $edgeType
   *   The edge type (data, trigger, tool_availability, loopback, etc.).
   * @param string $sourcePortName
   *   The source port name extracted from handle.
   * @param string $targetPortName
   *   The target port name extracted from handle.
   * @param array<string, mixed> $metadata
   *   Additional edge metadata.
   */
  public function __construct(
    private readonly string $id,
    private readonly string $source,
    private readonly string $target,
    private readonly string $sourceHandle,
    private readonly string $targetHandle,
    private readonly string $edgeType,
    private readonly string $sourcePortName,
    private readonly string $targetPortName,
    private readonly array $metadata = [],
  ) {}

  /**
   * Creates a DependencyEdge from a WorkflowEdgeDTO.
   *
   * @param object $edge
   *   The workflow edge DTO.
   *
   * @return self
   *   A new DependencyEdge instance.
   */
  public static function fromWorkflowEdge(object $edge): self {
    $sourceHandle = $edge->getSourceHandle();
    $targetHandle = $edge->getTargetHandle();

    // Determine edge type from metadata or handle patterns.
    $edgeType = self::determineEdgeType($edge);

    // Extract port names from handles.
    $sourcePortName = self::extractPortName($sourceHandle, "output");
    $targetPortName = self::extractPortName($targetHandle, "input");

    // Build metadata from edge data.
    $metadata = [];
    if (method_exists($edge, "getData")) {
      $metadata = $edge->getData();
    }
    if (method_exists($edge, "getBranchName")) {
      $branchName = $edge->getBranchName();
      if (!empty($branchName)) {
        $metadata["branchName"] = $branchName;
      }
    }

    return new self(
      id: $edge->getId(),
      source: $edge->getSource(),
      target: $edge->getTarget(),
      sourceHandle: $sourceHandle,
      targetHandle: $targetHandle,
      edgeType: $edgeType,
      sourcePortName: $sourcePortName,
      targetPortName: $targetPortName,
      metadata: $metadata,
    );
  }

  /**
   * Determines the edge type from edge metadata and handle patterns.
   *
   * @param object $edge
   *   The workflow edge.
   *
   * @return string
   *   The determined edge type.
   */
  private static function determineEdgeType(object $edge): string {
    // Check metadata for explicit edge type.
    if (method_exists($edge, "getData")) {
      $data = $edge->getData();
      if (isset($data["edgeType"]) && is_string($data["edgeType"])) {
        return $data["edgeType"];
      }
    }

    // Otherwise classify by the target node's input-port handle, the way the
    // editor encodes the connection. A tool connection is drawn
    // tool.output -> consumer.input, so the consumer's tool input handle
    // ("{nodeId}-input-tool") identifies it — mirroring trigger/loopback.
    // The suffix rules are owned by the shared classifier so job generation
    // (flowdrop_pipeline) and the compiler stay in lockstep.
    $targetHandle = $edge->getTargetHandle();
    if (EdgePortClassifier::isTriggerTarget($targetHandle)) {
      return self::TYPE_TRIGGER;
    }
    if (EdgePortClassifier::isLoopbackTarget($targetHandle)) {
      return self::TYPE_LOOPBACK;
    }
    if (EdgePortClassifier::isToolTarget($targetHandle)) {
      return self::TYPE_TOOL_AVAILABILITY;
    }

    // Default to data edge.
    return self::TYPE_DATA;
  }

  /**
   * Extracts port name from a handle string.
   *
   * Handle format: {nodeId}-{direction}-{portName}
   * Example: "text_input.1-output-text" → "text"
   *
   * @param string $handle
   *   The handle string.
   * @param string $direction
   *   The direction ("output" or "input").
   *
   * @return string
   *   The port name, or empty string if parsing failed.
   */
  private static function extractPortName(string $handle, string $direction): string {
    return EdgePortClassifier::portName($handle, $direction);
  }

  /**
   * Gets the edge ID.
   *
   * @return string
   *   The edge ID.
   */
  public function getId(): string {
    return $this->id;
  }

  /**
   * Gets the source node ID.
   *
   * @return string
   *   The source node ID.
   */
  public function getSource(): string {
    return $this->source;
  }

  /**
   * Gets the target node ID.
   *
   * @return string
   *   The target node ID.
   */
  public function getTarget(): string {
    return $this->target;
  }

  /**
   * Gets the source handle.
   *
   * @return string
   *   The source handle.
   */
  public function getSourceHandle(): string {
    return $this->sourceHandle;
  }

  /**
   * Gets the target handle.
   *
   * @return string
   *   The target handle.
   */
  public function getTargetHandle(): string {
    return $this->targetHandle;
  }

  /**
   * Gets the edge type.
   *
   * @return string
   *   The edge type.
   */
  public function getEdgeType(): string {
    return $this->edgeType;
  }

  /**
   * Gets the source port name.
   *
   * @return string
   *   The source port name.
   */
  public function getSourcePortName(): string {
    return $this->sourcePortName;
  }

  /**
   * Gets the target port name.
   *
   * @return string
   *   The target port name.
   */
  public function getTargetPortName(): string {
    return $this->targetPortName;
  }

  /**
   * Gets the edge metadata.
   *
   * @return array<string, mixed>
   *   The metadata.
   */
  public function getMetadata(): array {
    return $this->metadata;
  }

  /**
   * Gets a specific metadata value.
   *
   * @param string $key
   *   The metadata key.
   * @param mixed $default
   *   The default value if key doesn't exist.
   *
   * @return mixed
   *   The metadata value.
   */
  public function getMetadataValue(string $key, mixed $default = NULL): mixed {
    return $this->metadata[$key] ?? $default;
  }

  /**
   * Checks if this is a data edge.
   *
   * @return bool
   *   TRUE if this is a data edge.
   */
  public function isData(): bool {
    return $this->edgeType === self::TYPE_DATA;
  }

  /**
   * Checks if this is a trigger edge.
   *
   * @return bool
   *   TRUE if this is a trigger edge.
   */
  public function isTrigger(): bool {
    return $this->edgeType === self::TYPE_TRIGGER;
  }

  /**
   * Checks if this is a tool availability edge.
   *
   * @return bool
   *   TRUE if this is a tool availability edge.
   */
  public function isToolAvailability(): bool {
    return $this->edgeType === self::TYPE_TOOL_AVAILABILITY;
  }

  /**
   * Checks if this is a loopback edge.
   *
   * @return bool
   *   TRUE if this is a loopback edge.
   */
  public function isLoopback(): bool {
    return $this->edgeType === self::TYPE_LOOPBACK;
  }

  /**
   * Checks if this is an agent result edge.
   *
   * @return bool
   *   TRUE if this is an agent result edge.
   */
  public function isAgentResult(): bool {
    return $this->edgeType === self::TYPE_AGENT_RESULT;
  }

  /**
   * Checks if this edge should be excluded from execution graph.
   *
   * Certain edge types (tool_availability, loopback, agent_result) are
   * excluded from the execution graph as they don't represent direct
   * execution dependencies.
   *
   * @return bool
   *   TRUE if this edge should be excluded from execution graph.
   */
  public function isExcludedFromExecution(): bool {
    return in_array($this->edgeType, [
      self::TYPE_TOOL_AVAILABILITY,
      self::TYPE_LOOPBACK,
      self::TYPE_AGENT_RESULT,
    ], TRUE);
  }

  /**
   * Converts to array format.
   *
   * @return array<string, mixed>
   *   Array representation of the edge.
   */
  public function toArray(): array {
    return [
      "id" => $this->id,
      "source" => $this->source,
      "target" => $this->target,
      "sourceHandle" => $this->sourceHandle,
      "targetHandle" => $this->targetHandle,
      "edgeType" => $this->edgeType,
      "sourcePortName" => $this->sourcePortName,
      "targetPortName" => $this->targetPortName,
      "metadata" => $this->metadata,
    ];
  }

}
