<?php

declare(strict_types=1);

namespace Drupal\flowdrop_ai_context\Plugin\FlowDropNodeProcessor;

use Drupal\ai_context\Model\AiContextRequest;
use Drupal\ai_context\Model\AiContextRequestParamsData;
use Drupal\ai_context\Model\AiContextResult;
use Drupal\ai_context\Service\AiContextRequestFactory;
use Drupal\ai_context\Service\AiContextSelectorInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Dynamically selects the most relevant AI Context Items for a given task.
 *
 * Candidates can be narrowed by scope subscriptions, a target entity, and
 * always/never-include overrides, or merged from an agent's saved Context
 * Control Center configuration. Connect rendered_text to additional_context
 * on an AI Agent Executor node.
 */
#[FlowDropNodeProcessor(
  id: 'ai_context_dynamic_selector',
  label: new TranslatableMarkup('AI Context'),
  description: 'Automatically pick the most relevant AI Context Items for a given task',
  version: '1.0.0',
)]
final class AiContextDynamicSelector extends AiContextProcessorBase {

  /**
   * Constructs an AiContextDynamicSelector processor.
   */
  public function __construct(
    array $configuration,
    string $plugin_id,
    array $plugin_definition,
    protected readonly AiContextSelectorInterface $selector,
    protected readonly AiContextRequestFactory $requestFactory,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $instance = new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('ai_context.selector'),
      $container->get('ai_context.request_factory'),
    );
    $instance->setUsageTracker($container->get('ai_context.usage_tracker'));
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'task' => [
          'type' => 'string',
          'title' => 'Task',
          'description' => 'The task or prompt to select context for. Connect from an upstream node.',
          'format' => 'textarea',
        ],
        'agent_id' => [
          'type' => 'string',
          'title' => 'Inherit Agent Context Config',
          'description' => "Optional. When set, uses this agent's saved Context Control Center configuration (scopes, overrides, limits) and the manual Scope Subscriptions below are ignored. Only config-based agents carry context configuration. Leave empty to use the manual scopes.",
          'default' => '',
          'format' => 'ai_context_agent_selector',
        ],
        'scope_subscriptions' => [
          'type' => 'object',
          'title' => 'Scope Subscriptions',
          'description' => 'Restrict candidates to items matching these scope values (use case, tag, language, etc.). Leave empty to consider all published items. Ignored when an agent is selected above — that agent\'s scopes are used instead.',
          'default' => [],
          'format' => 'ai_context_scope_subscriptions',
        ],
        'entity_type' => [
          'type' => 'string',
          'title' => 'Target Entity Type',
          'description' => 'Optional entity type (e.g. node) for target-entity context matching. Connect from an upstream node, used together with Target Entity ID.',
          'default' => '',
        ],
        'entity_id' => [
          'type' => 'string',
          'title' => 'Target Entity ID',
          'description' => 'Optional entity ID for target-entity context matching. Used together with Target Entity Type.',
          'default' => '',
        ],
        'always_include' => [
          'type' => 'array',
          'title' => 'Always Include',
          'description' => 'Context item IDs to always include, bypassing scope filtering (must still be published).',
          'default' => [],
        ],
        'never_include' => [
          'type' => 'array',
          'title' => 'Never Include',
          'description' => 'Context item IDs to always exclude.',
          'default' => [],
        ],
        'selection_mode' => [
          'type' => 'string',
          'title' => 'Selection Mode',
          'description' => "How to behave when no scope subscriptions match. 'match_all' considers every published candidate; 'minimal' includes only global, entity-auto, and override items. Empty = auto (match_all, or the agent's configured mode when an agent is set).",
          'default' => '',
          'enum' => ['', 'minimal', 'match_all'],
        ],
        'max_items' => [
          'type' => 'integer',
          'title' => 'Max Items',
          'description' => 'Maximum number of context items to select. 0 = use module default (typically 5).',
          'default' => 0,
          'minimum' => 0,
        ],
        'max_tokens' => [
          'type' => 'integer',
          'title' => 'Max Tokens',
          'description' => 'Token budget for the rendered context block. 0 = use module default.',
          'default' => 0,
          'minimum' => 0,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'rendered_text' => [
          'type' => 'string',
          'description' => 'AI-ready structured block (id/label/purpose/guidance). Connect to additional_context on AI Agent Executor.',
        ],
        'selected_ids' => [
          'type' => 'array',
          'description' => 'String IDs of the selected context items.',
        ],
        'selected_items' => [
          'type' => 'array',
          'description' => 'Selected items with full metadata (id, label, purpose, scope, tags, subcontext_type) for downstream branching or display.',
        ],
        'item_count' => [
          'type' => 'integer',
          'description' => 'Number of context items selected.',
        ],
        'tokens_used' => [
          'type' => 'integer',
          'description' => 'Approximate tokens consumed by rendered_text.',
        ],
        'success' => [
          'type' => 'boolean',
          'description' => 'Whether selection completed without error. Empty results are not errors.',
        ],
        'error_message' => [
          'type' => 'string',
          'description' => 'Error details if selection failed.',
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    if (empty($params['task'])) {
      return ValidationResult::error('task', 'Task is required', 'MISSING_TASK');
    }
    return ValidationResult::success();
  }

  /**
   * Resolves the selection mode to send to the request factory.
   *
   * The `selection_mode` parameter defaults to '' meaning "auto", which this
   * node's schema documents as "match_all, or the agent's configured mode when
   * an agent is set". ai_context spells "auto" as NULL and normalises anything
   * it does not recognise — '' included — to 'minimal', which keeps only
   * global, entity-auto and override items. Passing '' straight through
   * therefore silently drops every scope-matched item and returns an empty
   * context block with success: TRUE.
   *
   * @param \Drupal\flowdrop\DTO\ParameterBagInterface $params
   *   The resolved node parameters.
   * @param bool $agentHasContextConfig
   *   Whether the selected agent carries Context Control Center config, and so
   *   will supply its own mode.
   *
   * @return string|null
   *   An explicit mode, or NULL to let the agent's configured mode apply.
   */
  private function resolveSelectionMode(ParameterBagInterface $params, bool $agentHasContextConfig): ?string {
    $mode = trim($params->getString('selection_mode', ''));
    if ($mode !== '') {
      return $mode;
    }

    return $agentHasContextConfig
      ? NULL
      : AiContextRequest::SELECTION_MODE_MATCH_ALL;
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $maxItems = $params->getInt('max_items', 0);
    $maxTokens = $params->getInt('max_tokens', 0);
    $agentId = trim($params->getString('agent_id', ''));
    $entityType = trim($params->getString('entity_type', ''));
    $entityId = trim($params->getString('entity_id', ''));
    $hasEntity = $entityType !== '' && is_numeric($entityId);

    // Scope source is mutually exclusive, but only when the selected agent
    // actually carries Context Control Center config (config-based agents).
    // Plugin agents have no config to inherit, so the node's manual scope
    // subscriptions must still apply.
    $agentHasContextConfig = $agentId !== ''
      && $this->requestFactory->findAgentConfig($agentId) !== [];
    $scopeSubscriptions = $agentHasContextConfig
      ? []
      : $params->getArray('scope_subscriptions', []);

    try {
      $paramsData = AiContextRequestParamsData::fromArray([
        'task' => $params->getString('task'),
        'consumer_id' => $this->consumerId(),
        'scope_subscriptions' => $scopeSubscriptions,
        'agent_id' => $agentId,
        'entity_type' => $entityType !== '' ? $entityType : NULL,
        'entity_id' => $hasEntity ? $entityId : NULL,
        'always_include' => $params->getArray('always_include', []),
        'never_include' => $params->getArray('never_include', []),
        'selection_mode' => $this->resolveSelectionMode($params, $agentHasContextConfig),
        'max_items' => $maxItems > 0 ? $maxItems : NULL,
        'max_tokens' => $maxTokens > 0 ? $maxTokens : NULL,
      ]);
      $request = $this->requestFactory->fromParameters($paramsData);

      $result = $this->selector->select($request);

      $this->recordContextUsage(
        $result->getSelectedItemIds(),
        $agentId,
        $hasEntity ? $entityType : NULL,
        $hasEntity ? $entityId : NULL,
      );

      return $this->successResult([
        'rendered_text' => $result->getRenderedText(),
        'selected_ids' => $result->getSelectedItemIds(),
        'selected_items' => $this->buildSelectedItems($result),
        'item_count' => count($result->getSelectedItemIds()),
        'tokens_used' => $result->getTokensUsed(),
      ]);
    }
    catch (\Exception $e) {
      return $this->failureResult([
        'rendered_text' => '',
        'selected_ids' => [],
        'selected_items' => [],
        'item_count' => 0,
        'tokens_used' => 0,
      ], $e->getMessage());
    }
  }

  /**
   * Flattens the result's selected items into an id-keyed list.
   *
   * @param \Drupal\ai_context\Model\AiContextResult $result
   *   The selection result.
   *
   * @return array<int, array<string, mixed>>
   *   A list of items, each merged with its string 'id', preserving the
   *   full per-item metadata (label, purpose, scope, tags, etc.).
   */
  private function buildSelectedItems(AiContextResult $result): array {
    $items = [];
    foreach ($result->getSelectedItems(TRUE) as $id => $data) {
      $items[] = ['id' => (string) $id] + $data;
    }
    return $items;
  }

}
