<?php

declare(strict_types=1);

namespace Drupal\flowdrop_ai_context\Plugin\FlowDropNodeProcessor;

use Drupal\ai_context\Service\AiContextUsageTracker;
use Drupal\flowdrop\DTO\ExecutionContextDTO;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface;

/**
 * Base class for FlowDrop node processors that consume AI Context.
 *
 * Centralizes the success/error result envelope shared by all AI Context
 * nodes and execution-context awareness. The execution context lets the
 * nodes derive a stable consumer identifier (for AI Context request logging)
 * and a runner identifier (for usage tracking), so workflow-driven context
 * injections are traceable in the Context Control Center.
 *
 * @see \Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface
 */
abstract class AiContextProcessorBase extends AbstractFlowDropNodeProcessor implements ExecutionContextAwareInterface {

  /**
   * The execution context, injected by the runtime before process().
   */
  protected ?ExecutionContextDTO $executionContext = NULL;

  /**
   * The Context Control Center usage tracker, set in the subclass create().
   */
  protected ?AiContextUsageTracker $usageTracker = NULL;

  /**
   * {@inheritdoc}
   */
  public function setExecutionContext(ExecutionContextDTO $context): void {
    $this->executionContext = $context;
  }

  /**
   * Sets the usage tracker. Called from the subclass create() method.
   */
  public function setUsageTracker(AiContextUsageTracker $usageTracker): void {
    $this->usageTracker = $usageTracker;
  }

  /**
   * Records Context Control Center usage for the items this node injected.
   *
   * No-ops when tracking is disabled, when there is no execution context to
   * derive a runner ID from, or when there are no items to record.
   *
   * @param string[] $contextIds
   *   IDs of the context items that were injected.
   * @param string $agentId
   *   The agent to attribute usage to. Falls back to the node's consumer ID
   *   when empty, so workflow injections are still recorded.
   * @param string|null $entityType
   *   Optional target entity type to attach to the usage records.
   * @param int|string|null $entityId
   *   Optional target entity ID, used together with $entityType.
   */
  protected function recordContextUsage(
    array $contextIds,
    string $agentId = '',
    ?string $entityType = NULL,
    int|string|null $entityId = NULL,
  ): void {
    if ($this->usageTracker === NULL || !$this->usageTracker->isTrackingEnabled()) {
      return;
    }

    $runnerId = $this->runnerId();
    $agentId = $agentId !== '' ? $agentId : $this->consumerId();
    if ($runnerId === '' || $contextIds === []) {
      return;
    }

    $this->usageTracker->recordUsage($contextIds, $agentId, $runnerId);

    if ($entityType !== NULL && $entityType !== '' && is_numeric($entityId)) {
      $this->usageTracker->attachEntity($runnerId, $entityType, (int) $entityId);
    }
  }

  /**
   * Builds a consumer identifier for AI Context request logging.
   *
   * @return string
   *   An identifier of the form "flowdrop:<workflow>:<node>", falling back to
   *   "flowdrop:<plugin_id>" when no execution context has been injected.
   */
  protected function consumerId(): string {
    if ($this->executionContext === NULL) {
      return 'flowdrop:' . $this->getId();
    }
    $workflowId = $this->executionContext->getWorkflowId() ?: 'unknown';
    $nodeId = $this->executionContext->getNodeId() ?: $this->getId();
    return 'flowdrop:' . $workflowId . ':' . $nodeId;
  }

  /**
   * Returns the runner identifier used for usage tracking.
   *
   * @return string
   *   The pipeline ID when available, else the workflow ID, else an empty
   *   string when no execution context has been injected.
   */
  protected function runnerId(): string {
    if ($this->executionContext === NULL) {
      return '';
    }
    return $this->executionContext->getPipelineId()
      ?: $this->executionContext->getWorkflowId();
  }

  /**
   * Wraps node-specific output as a successful result.
   *
   * @param array<string, mixed> $data
   *   Output fields specific to the node.
   *
   * @return array<string, mixed>
   *   The output with the success/error_message envelope applied.
   */
  protected function successResult(array $data): array {
    return array_merge($data, [
      'success' => TRUE,
      'error_message' => '',
    ]);
  }

  /**
   * Wraps node-specific output as a failed result.
   *
   * @param array<string, mixed> $data
   *   Default output fields for the failure case (e.g. empty strings).
   * @param string $message
   *   Human-readable error message.
   *
   * @return array<string, mixed>
   *   The output with the success/error_message envelope applied.
   */
  protected function failureResult(array $data, string $message): array {
    return array_merge($data, [
      'success' => FALSE,
      'error_message' => $message,
    ]);
  }

}
