<?php

namespace Drupal\ai_automators\PluginBaseClasses;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\ai\AiProviderPluginManager;
use Drupal\ai\Dto\StructuredOutputSchema;
use Drupal\ai\OperationType\Chat\ChatInput;
use Drupal\ai\OperationType\Chat\ChatMessage;
use Drupal\ai\OperationType\GenericType\ImageFile;
use Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface;
use Drupal\ai\Utility\CastUtility;
use Drupal\ai_automators\AiAutomatorInterface;
use Drupal\ai_automators\Exceptions\AiAutomatorResponseErrorException;
use Drupal\ai_automators\Exceptions\AiAutomatorTypeNotRunnable;
use Drupal\ai_automators\PluginInterfaces\AiAutomatorPostCheckIfEmptyInterface;
use Drupal\ai_automators\PluginInterfaces\AiAutomatorTypeInterface;
use Drupal\ai_automators\Traits\GeneralHelperTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * This is a base class for all LLM rule helpers.
 *
 * @todo this should be the AiAutomatorBase class.
 */
abstract class RuleBase extends PluginBase implements AiAutomatorTypeInterface, AiAutomatorPostCheckIfEmptyInterface, ContainerFactoryPluginInterface {

  use GeneralHelperTrait;

  /**
   * The LLM type.
   *
   * @var string
   */
  protected string $llmType = 'chat';

  /**
   * The json schema.
   *
   * @var array<mixed>
   */
  public array $jsonSchema = [];

  /**
   * The plugin manager.
   *
   * @var \Drupal\ai\AiProviderPluginManager
   */
  protected AiProviderPluginManager $aiPluginManager;


  /**
   * The prompt JSON decoder.
   *
   * @var \Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface
   */
  protected PromptJsonDecoderInterface $promptJsonDecoder;

  /**
   * The automator type ID.
   *
   * @var string
   */
  protected $uuid;

  /**
   * The weight of the automator type.
   *
   * @var int|string
   */
  protected $weight = '';

  /**
   * The custom label for the automator type.
   *
   * @var string|null
   */
  protected $label = NULL;

  /**
   * Constructs a new AiClientBase abstract class.
   *
   * @param array<string,mixed> $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\ai\AiProviderPluginManager $pluginManager
   *   The plugin manager.
   * @param \Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface $promptJsonDecoder
   *   The prompt JSON decoder.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    AiProviderPluginManager $pluginManager,
    PromptJsonDecoderInterface $promptJsonDecoder,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->aiPluginManager = $pluginManager;
    $this->promptJsonDecoder = $promptJsonDecoder;
    $this->setConfiguration($configuration);
  }

  /**
   * Load from dependency injection container.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container.
   * @param array<string,mixed> $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('ai.provider'),
      $container->get('ai.prompt_json_decode')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function label(): string {
    // The plugin definition is not typed as a string array.
    // @phpstan-ignore-next-line
    return $this->label ?? $this->pluginDefinition['label'];
  }

  /**
   * {@inheritDoc}
   */
  public function defaultConfiguration() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getUuid() {
    return $this->uuid;
  }

  /**
   * {@inheritdoc}
   */
  public function setWeight($weight) {
    $this->weight = $weight;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getWeight() {
    return $this->weight;
  }

  /**
   * {@inheritdoc}
   */
  public function getConfiguration() {
    return [
      'uuid' => $this->getUuid(),
      'id' => $this->getPluginId(),
      'weight' => $this->getWeight(),
      'settings' => $this->configuration,
      'label' => $this->label(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function setConfiguration(array $configuration) {
    $configuration += [
      'settings' => [],
      'uuid' => '',
      'weight' => 0,
      'label' => NULL,
    ];
    $this->configuration = $configuration['settings'] + $this->defaultConfiguration();
    $this->uuid = $configuration['uuid'];
    $this->weight = $configuration['weight'];
    $this->label = $configuration['label'];
    return $this;
  }

  /**
   * {@inheritDoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state, AiAutomatorInterface $automator): array {
    return $form;
  }

  /**
   * {@inheritDoc}
   */
  public function buildAdvancedConfigurationForm(array $form, FormStateInterface $form_state, AiAutomatorInterface $automator): array {
    return $form;
  }

  /**
   * {@inheritDoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state, AiAutomatorInterface $automator): void {
  }

  /**
   * {@inheritDoc}
   */
  public function validateAdvancedConfigurationForm(array &$form, FormStateInterface $form_state, AiAutomatorInterface $automator): void {
  }

  /**
   * {@inheritDoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state, AiAutomatorInterface $automator): void {
    $form_state->cleanValues();
    foreach ($form_state->getValues() as $key => $value) {
      $this->configuration[$key] = $value;
    }
  }

  /**
   * {@inheritDoc}
   */
  public function submitAdvancedConfigurationForm(array &$form, FormStateInterface $form_state, AiAutomatorInterface $automator): void {
    $form_state->cleanValues();
    foreach ($form_state->getValues() as $key => $value) {
      $this->configuration[$key] = $value;
    }
  }

  /**
   * {@inheritDoc}
   */
  public function needsPrompt() {
    return TRUE;
  }

  /**
   * {@inheritDoc}
   */
  public function advancedMode() {
    return TRUE;
  }

  /**
   * {@inheritDoc}
   */
  public function checkIfEmpty(array $value, array $automatorConfig = []) {
    return $value;
  }

  /**
   * Optional post-check hook for complex empty-state normalization.
   *
   * Rules can override this to adjust values after checkIfEmpty().
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being worked on.
   * @param array $value
   *   The value response.
   * @param array $automatorConfig
   *   The automator config.
   *
   * @return array
   *   Returns an empty array if the value should be considered empty.
   */
  public function postCheckIfEmpty(ContentEntityInterface $entity, array $value, array $automatorConfig = []): array {
    return $value;
  }

  /**
   * {@inheritDoc}
   */
  public function placeholderText() {
    return 'Enter a prompt here.';
  }

  /**
   * {@inheritDoc}
   */
  public function ruleIsAllowed(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition) {
    return TRUE;
  }

  /**
   * {@inheritDoc}
   */
  public function helpText() {
    return "";
  }

  /**
   * {@inheritDoc}
   */
  public function allowedInputs() {
    return [
      'text_long',
      'text',
      'string',
      'string_long',
      'text_with_summary',
    ];
  }

  /**
   * {@inheritDoc}
   */
  public function tokens(ContentEntityInterface $entity) {
    return [
      'context' => 'The cleaned text from the base field.',
      'raw_context' => 'The raw text from the base field. Can include HTML',
      'max_amount' => 'The max amount of entries to set. If unlimited this value will be empty.',
    ];
  }

  /**
   * {@inheritDoc}
   */
  public function extraAdvancedFormFields(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition, FormStateInterface $formState, array $defaultValues = []): array {
    /** @var array<mixed> $form */
    $form = [];

    // Build the default value for the element from stored config.
    // Supports both the new provider_config structure and the legacy
    // ai_provider/ai_model flat keys for backwards compatibility.
    $defaultProviderConfig = NULL;
    if (!empty($defaultValues['provider_config']) && is_array($defaultValues['provider_config'])) {
      $defaultProviderConfig = $defaultValues['provider_config'];
    }
    elseif (!empty($defaultValues['ai_provider'])) {
      // BC: convert old flat keys to the format expected by the element.
      $defaultProviderConfig = [
        'provider' => $defaultValues['ai_provider'],
        'model'    => $defaultValues['ai_model'] ?? '',
        'config'   => [],
      ];
    }

    $form['provider_config'] = [
      '#type'                    => 'ai_provider_configuration',
      '#title'                   => $this->t('AI Provider'),
      '#operation_type'          => $this->llmType,
      '#default_value'           => $defaultProviderConfig,
      '#default_provider_allowed' => TRUE,
    ];

    // Vision-specific image field and style selectors.
    // We always render these and rely on JS/AJAX states to show/hide them
    // based on the selected provider/model.
    if ($this->llmType === 'chat') {
      $imageFields = $this->getGeneralHelper()->getImageMediaFields($entity);
      if (!empty($imageFields)) {
        $form['image_field'] = [
          '#type'          => 'select',
          '#title'         => $this->t('Image Field'),
          '#options'       => $imageFields,
          '#description'   => $this->t('For vision models, optionally add an image field to the prompt.'),
          '#empty_option'  => $this->t('No images'),
          '#default_value' => $defaultValues['image_field'] ?? NULL,
        ];

        $form['image_style'] = [
          '#type'          => 'select',
          '#title'         => $this->t('Image Style'),
          '#description'   => $this->t('Use an optional image style to lower costs and increase speed.'),
          '#empty_option'  => $this->t('Use original'),
          '#options'       => $this->getGeneralHelper()->getImageStyles(FALSE),
          '#default_value' => $defaultValues['image_style'] ?? NULL,
          '#states'        => [
            'visible' => [
              ':input[name="image_field"]' => ['!value' => ''],
            ],
          ],
        ];
      }
    }

    return $form;
  }

  /**
   * {@inheritDoc}
   */
  public function generateTokens(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition, array $automatorConfig, $delta = 0) {
    $values = $entity->get($automatorConfig['base_field'])->getValue();
    return [
      'context' => strip_tags($values[$delta]['value'] ?? ''),
      'raw_context' => $values[$delta]['value'] ?? '',
      'max_amount' => $fieldDefinition->getFieldStorageDefinition()->getCardinality() == -1 ? '' : $fieldDefinition->getFieldStorageDefinition()->getCardinality(),
    ];
  }

  /**
   * {@inheritDoc}
   */
  public function generate(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition, array $automatorConfig) {
    // Reset schema between runs.
    $this->jsonSchema = [];
    // Generate the real prompt if needed.
    $prompts = [];
    // @phpstan-ignore-next-line
    if (!empty($automatorConfig['mode']) && $automatorConfig['mode'] == 'token' && \Drupal::service('module_handler')->moduleExists('token')) {
      $prompts[] = \Drupal::service('ai_automator.prompt_helper')->renderTokenPrompt($automatorConfig['token'], $entity); /* @phpstan-ignore-line */
    }
    elseif ($this->needsPrompt()) {
      foreach ($entity->get($automatorConfig['base_field'])->getValue() as $i => $item) {
        // Get tokens.
        $tokens = $this->generateTokens($entity, $fieldDefinition, $automatorConfig, $i);
        $prompts[] = \Drupal::service('ai_automator.prompt_helper')->renderPrompt($automatorConfig['prompt'], $tokens, $i); /* @phpstan-ignore-line */
      }
    }
    return $prompts;
  }

  /**
   * {@inheritDoc}
   */
  public function verifyValue(ContentEntityInterface $entity, $value, FieldDefinitionInterface $fieldDefinition, array $automatorConfig) {
    return TRUE;
  }

  /**
   * {@inheritDoc}
   */
  public function storeValues(ContentEntityInterface $entity, array $values, FieldDefinitionInterface $fieldDefinition, array $automatorConfig) {
    $entity->set($fieldDefinition->getName(), $values);
  }

  /**
   * If a json schema is set, it returns it.
   *
   * @return array<mixed>|null
   *   The json schema.
   */
  public function getJsonSchema() {
    return !empty($this->jsonSchema) && count($this->jsonSchema) ? $this->jsonSchema : NULL;
  }

  /**
   * Enables structured output schema for the current execution.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being processed.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
   *   The field definition.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   */
  protected function setStructuredOutputSchema(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition, array $automatorConfig): void {
    $valueSchema = $this->getStructuredOutputValueSchema($entity, $fieldDefinition, $automatorConfig);
    if (!is_array($valueSchema) || !$valueSchema) {
      $this->jsonSchema = [];
      return;
    }
    $this->jsonSchema = $this->buildStructuredOutputSchema($valueSchema, $fieldDefinition);
  }

  /**
   * Enables a custom structured output schema for the current execution.
   *
   * @param array<mixed> $valueSchema
   *   The schema for one value item.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
   *   The field definition.
   * @param string|null $schemaName
   *   Optional schema name override.
   */
  protected function setStructuredOutputSchemaFromValueSchema(array $valueSchema, FieldDefinitionInterface $fieldDefinition, ?string $schemaName = NULL): void {
    $this->jsonSchema = $this->buildStructuredOutputSchema($valueSchema, $fieldDefinition, $schemaName);
  }

  /**
   * Gets the value schema for one response item.
   *
   * By default this is derived from the destination field type.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being processed.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
   *   The field definition.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   *
   * @return array<mixed>
   *   JSON schema for one value.
   */
  protected function getStructuredOutputValueSchema(ContentEntityInterface $entity, FieldDefinitionInterface $fieldDefinition, array $automatorConfig): array {
    return $this->getValueSchemaForFieldType($fieldDefinition->getType(), $fieldDefinition);
  }

  /**
   * Maps a Drupal field type to the corresponding JSON schema value type.
   *
   * @param string $fieldType
   *   Drupal field type id.
   * @param \Drupal\Core\Field\FieldDefinitionInterface|null $fieldDefinition
   *   Optional field definition for settings lookups.
   *
   * @return array<mixed>
   *   JSON schema for one value.
   */
  protected function getValueSchemaForFieldType(string $fieldType, ?FieldDefinitionInterface $fieldDefinition = NULL): array {
    switch ($fieldType) {
      case 'boolean':
        return [
          'type' => 'boolean',
        ];

      case 'integer':
        return [
          'type' => 'integer',
        ];

      case 'decimal':
      case 'float':
        return [
          'type' => 'number',
        ];

      case 'email':
        return [
          'type' => 'string',
          'format' => 'email',
        ];

      case 'telephone':
        return [
          'type' => 'string',
          'pattern' => '^\\+?[0-9\\s\\-()]{6,20}$',
        ];

      case 'link':
        $titleSetting = (int) ($fieldDefinition?->getSetting('title') ?? 0);
        $required = ['uri'];
        $properties = [
          'uri' => [
            'type' => 'string',
          ],
        ];
        // Strict-mode providers require every key in "properties" to also
        // appear in "required". Expose title whenever the field allows it
        // (optional = 1, required = 2) so the LLM knows it can provide a
        // value; Drupal validation still permits omitting it when optional.
        if ($titleSetting >= 1) {
          $required[] = 'title';
          $properties['title'] = [
            'type' => 'string',
          ];
        }
        return [
          'type' => 'object',
          'additionalProperties' => FALSE,
          'required' => $required,
          'properties' => $properties,
        ];
    }

    // List fields can return either key or label in existing automators.
    if (str_starts_with($fieldType, 'list_')) {
      $schema = [
        'type' => 'string',
      ];
      $allowedValues = $fieldDefinition?->getSetting('allowed_values');
      if (is_array($allowedValues) && !empty($allowedValues)) {
        $options = array_map('strval', array_unique(array_merge(array_keys($allowedValues), array_values($allowedValues))));
        if (!empty($options)) {
          $schema['enum'] = $options;
        }
      }
      return $schema;
    }

    return [
      'type' => 'string',
    ];
  }

  /**
   * Builds the full structured response schema in wrapped "values" format.
   *
   * @param array<mixed> $valueSchema
   *   The schema of the "value" field for each item.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
   *   The field definition.
   * @param string|null $schemaName
   *   Optional schema name override.
   *
   * @return array<mixed>
   *   The full schema for ChatInput::setChatStructuredJsonSchema().
   */
  protected function buildStructuredOutputSchema(array $valueSchema, FieldDefinitionInterface $fieldDefinition, ?string $schemaName = NULL): array {
    $defaultName = strtolower((string) preg_replace('/[^a-z0-9_]+/', '_', $this->getPluginId() . '_' . $fieldDefinition->getName()));
    $name = $schemaName ?: ($defaultName ?: 'automator_output');
    return [
      'name' => $name,
      'strict' => TRUE,
      'schema' => [
        'type' => 'object',
        'additionalProperties' => FALSE,
        'required' => ['values'],
        'properties' => [
          'values' => [
            'type' => 'array',
            'items' => [
              'type' => 'object',
              'additionalProperties' => FALSE,
              'required' => ['value'],
              'properties' => [
                'value' => $valueSchema,
              ],
            ],
          ],
        ],
      ],
    ];
  }

  /**
   * Ajax callback to load the models for the selected provider.
   *
   * @param array<mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array<string,mixed>
   *   The form array.
   *
   * @deprecated in ai_automators:2.0.0 and is removed from ai_automators:3.0.0.
   *   The ai_provider_configuration form element
   *   manages its own AJAX internally.
   * @see https://www.drupal.org/project/ai/issues/3577173
   */
  public static function loadModelsAjaxCallback(array &$form, FormStateInterface $form_state) {
    @trigger_error('RuleBase::loadModelsAjaxCallback() is deprecated in ai_automators:2.0.0 and is removed from ai_automators:3.0.0. The ai_provider_configuration form element handles its own AJAX. See https://www.drupal.org/project/ai/issues/3577173', E_USER_DEPRECATED);
    $form_state->setRebuild(TRUE);
    $trigger = $form_state->getTriggeringElement();
    $suffix = $trigger['#attributes']['data-trigger-suffix'] ?? '';
    return $form['automator_container']['automator_advanced']['ajax_prefix' . $suffix] ?? [];
  }

  /**
   * Load one extra provider form.
   *
   * Adds an ai_provider_configuration element to $form keyed by a derived
   * config key (provider_config_{suffix}). This replaces the old manual
   * provider/model/settings dropdowns.
   *
   * @param array<mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The form state.
   * @param string $type
   *   The operation type.
   * @param string $suffix
   *   The suffix (used to derive a unique config key).
   * @param string $title
   *   The title.
   * @param array<mixed> $defaultValues
   *   The default values (settings array from the stored configuration).
   *
   * @return array<mixed>
   *   The form array.
   */
  public function extraProviderForm(&$form, FormStateInterface $formState, $type, $suffix, $title, $defaultValues = []) {
    $suffix = '_' . ltrim($suffix, '_');
    $configKey = 'provider_config' . $suffix;

    // Build default value from stored config.
    // Supports both the new provider_config_{suffix} structure and the legacy
    // ai_provider_{suffix}/ai_model_{suffix} flat keys.
    $defaultProviderConfig = NULL;
    if (!empty($defaultValues[$configKey]) && is_array($defaultValues[$configKey])) {
      $defaultProviderConfig = $defaultValues[$configKey];
    }
    elseif (!empty($defaultValues['ai_provider' . $suffix])) {
      // BC: convert old flat keys to the format expected by the element.
      $defaultProviderConfig = [
        'provider' => $defaultValues['ai_provider' . $suffix],
        'model'    => $defaultValues['ai_model' . $suffix] ?? '',
        'config'   => [],
      ];
    }

    $form[$configKey] = [
      '#type'                    => 'ai_provider_configuration',
      '#title'                   => $title,
      '#operation_type'          => $type,
      '#default_value'           => $defaultProviderConfig,
      '#default_provider_allowed' => TRUE,
    ];

    return $form;
  }

  /**
   * Prepare LLM Instance.
   *
   * @param string $operationType
   *   The operation type.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   *
   * @return \Drupal\ai\OperationType\Chat\ChatInterface
   *   The LLM instance.
   */
  public function prepareLlmInstance($operationType, array &$automatorConfig) {
    $provider = $this->getProvider($automatorConfig);
    $model = $this->getModel($automatorConfig);
    // @todo fix when the proxy is gone.
    /** @var \Drupal\ai\AiProviderInterface&\Drupal\ai\OperationType\Chat\ChatInterface $instance */
    // @phpstan-ignore varTag.nativeType
    $instance = $this->aiPluginManager->createInstance($provider);

    // Read configuration from new standardized provider_config structure.
    $config = [];
    $providerConfigData = $automatorConfig['provider_config'] ?? [];
    if (!empty($providerConfigData['config']) && is_array($providerConfigData['config'])) {
      $config = $providerConfigData['config'];
    }
    else {
      // BC: fall back to old flat ai_provider_* keys.
      $configCast = $instance->getAvailableConfiguration($operationType, $model);
      foreach ($automatorConfig as $key => $val) {
        if (str_starts_with($key, 'ai_provider_') && $val) {
          $configKey = substr($key, strlen('ai_provider_'));
          if (isset($configCast[$configKey]['type'])) {
            $config[$configKey] = CastUtility::typeCast($configCast[$configKey]['type'], $val);
          }
        }
      }
    }
    $instance->setConfiguration($config);
    return $instance;
  }

  /**
   * Run a chat message.
   *
   * @param string $prompt
   *   The prompt.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   * @param \Drupal\ai\OperationType\Chat\ChatInterface $instance
   *   The LLM instance.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity.
   *
   * @return array<string,mixed>
   *   The response.
   */
  public function runChatMessage(string $prompt, array $automatorConfig, $instance, ?ContentEntityInterface $entity = NULL) {
    /** @var \Drupal\ai\OperationType\Chat\ChatMessage $text */
    $text = $this->runRawChatMessage($prompt, $automatorConfig, $instance, $entity);

    // Normalize the response.
    $json = $this->promptJsonDecoder->decode($text);
    if (!is_array($json)) {
      throw new AiAutomatorResponseErrorException('The response was not a valid JSON response. The response was: ' . $text->getText());
    }
    if ($this->getJsonSchema()) {
      return $this->decodeStructuredResponse($json);
    }
    return $this->decodeValueArray($json);
  }

  /**
   * Run a chat message.
   *
   * @param string $prompt
   *   The prompt.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   * @param \Drupal\ai\OperationType\Chat\ChatInterface $instance
   *   The LLM instance.
   * @param \Drupal\Core\Entity\ContentEntityInterface|null $entity
   *   The entity.
   *
   * @return \Drupal\ai\OperationType\Chat\ChatMessage
   *   The response.
   */
  public function runRawChatMessage(string $prompt, array $automatorConfig, $instance, ?ContentEntityInterface $entity = NULL) {
    $images = [];
    // Check for images or media.
    $possibleImages = [];
    if (!empty($automatorConfig['image_field'])) {
      $parts[0] = $automatorConfig['image_field'];
      if (strpos($automatorConfig['image_field'], '--') !== FALSE) {
        $parts = explode('--', $automatorConfig['image_field']);
      }

      /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $imageEntityWrapper */
      foreach ($entity->get($parts[0]) as $imageEntityWrapper) {
        /** @var \Drupal\file\FileInterface|null $imageEntity */
        $imageEntity = $imageEntityWrapper->entity;
        // If the image entity is not available, it might be partially formed.
        if (!$imageEntity) {
          $values = $imageEntityWrapper->getValue();
          // Try to load via the file ID.
          // @phpstan-ignore-next-line
          if (isset($values['target_id']) && $file = \Drupal::entityTypeManager()->getStorage('file')->load($values['target_id'])) {
            $imageEntity = $file;
          }
        }

        if (isset($parts[1])) {
          /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $image */
          foreach ($imageEntity->get($parts[1]) as $image) {
            $possibleImages[] = $image->entity;
          }
        }
        else {
          $possibleImages[] = $imageEntity;
        }
      }
    }
    foreach ($possibleImages as $possibleImage) {
      // If an image style is set, use it.
      if (!empty($automatorConfig['image_style'])) {
        $possibleImage = $this->getGeneralHelper()->preprocessImageStyle($possibleImage, $automatorConfig['image_style']);
      }
      $image = new ImageFile();
      $image->setFileFromFile($possibleImage);
      $images[] = $image;
    }

    // Create new messages.
    $input = new ChatInput([
      new ChatMessage("user", $prompt, $images),
    ]);

    if ($this->getJsonSchema()) {
      $schema = StructuredOutputSchema::fromArray($this->getJsonSchema());
      $input->setChatStructuredJsonSchema($schema);
    }

    $model = $this->getModel($automatorConfig);
    /** @var \Drupal\ai\OperationType\Chat\ChatMessage $response */
    $response = $instance->chat($input, $model, $this->getTags($prompt, $automatorConfig, $instance, $entity))->getNormalized();

    return $response;
  }

  /**
   * Get the provider.
   *
   * Reads from the new standardized provider_config structure.
   * Falls back to the old flat ai_provider key for backwards compatibility.
   *
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   *
   * @return string
   *   The provider ID.
   */
  protected function getProvider(array $automatorConfig): string {
    // New standardized structure.
    $providerConfig = $automatorConfig['provider_config'] ?? [];
    $provider = $providerConfig['provider'] ?? '';

    // BC: fall back to old flat ai_provider key.
    if (empty($provider) && !empty($automatorConfig['ai_provider'])) {
      $provider = $automatorConfig['ai_provider'];
    }

    if (empty($provider)) {
      throw new AiAutomatorTypeNotRunnable('No provider set for the LLM type ' . $this->llmType);
    }

    // Resolve the default pseudo-providers to real provider IDs.
    if ($provider === 'default_json') {
      $defaultProvider = $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_complex_json');
      $provider = $defaultProvider['provider_id'] ?? '';
    }
    elseif ($provider === 'default_structured_response') {
      $defaultProvider = $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_structured_response');
      $provider = $defaultProvider['provider_id'] ?? '';
    }
    elseif ($provider === 'default_vision') {
      $defaultProvider = $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_image_vision');
      $provider = $defaultProvider['provider_id'] ?? '';
    }
    elseif ($provider === 'default') {
      $defaultProvider = $this->aiPluginManager->getDefaultProviderForOperationType($this->llmType);
      $provider = $defaultProvider['provider_id'] ?? '';
    }

    if (empty($provider) || !is_string($provider)) {
      throw new AiAutomatorTypeNotRunnable('Invalid or missing AI provider for LLM type: ' . $this->llmType);
    }
    return $provider;
  }

  /**
   * Get the model.
   *
   * Reads from the new standardized provider_config structure.
   * Falls back to the old flat ai_model key for backwards compatibility.
   *
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   *
   * @return string
   *   The model ID.
   */
  protected function getModel(array &$automatorConfig): string {
    // New standardized structure.
    $providerConfig = $automatorConfig['provider_config'] ?? [];
    $model = $providerConfig['model'] ?? '';

    // Resolve pseudo-providers stored in the new structure. The old form
    // never persisted a model when one of the default_* options was picked,
    // so $providerConfig['model'] is empty for migrated automators that used
    // a default provider.
    if (empty($model) && !empty($providerConfig['provider'])) {
      $model = $this->resolvePseudoProviderModel($providerConfig['provider']);
    }

    // BC: fall back to the legacy ai_provider/ai_model flat keys for
    // automators that have not been migrated yet.
    if (empty($model)) {
      $legacyProvider = $automatorConfig['ai_provider'] ?? '';
      $model = $this->resolvePseudoProviderModel($legacyProvider);
      if (empty($model)) {
        $model = $automatorConfig['ai_model'] ?? '';
      }
    }

    return $model;
  }

  /**
   * Resolve a default_* pseudo-provider key to the configured model id.
   *
   * @param string $providerKey
   *   The provider key as stored in configuration. Anything other than the
   *   recognized default_* values returns an empty string.
   *
   * @return string
   *   The resolved model id, or an empty string if the key is not a known
   *   pseudo-provider or no default has been configured.
   */
  protected function resolvePseudoProviderModel(string $providerKey): string {
    return match ($providerKey) {
      'default_json' => $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_complex_json')['model_id'] ?? '',
      'default_structured_response' => $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_structured_response')['model_id'] ?? '',
      'default_vision' => $this->aiPluginManager->getDefaultProviderForOperationType('chat_with_image_vision')['model_id'] ?? '',
      'default' => $this->aiPluginManager->getDefaultProviderForOperationType($this->llmType)['model_id'] ?? '',
      default => '',
    };
  }

  /**
   * Decode a value array.
   *
   * @param mixed $json
   *   The input.
   *
   * @return array<mixed>
   *   The decoded array.
   */
  public function decodeValueArray($json) {
    // Sometimes it doesn't become a valid JSON response, but many.
    if (isset($json[0]['value'])) {
      $values = [];
      foreach ($json as $val) {
        if (isset($val['value'])) {
          $values[] = $val['value'];
        }
      }
      return $values;
    }
    // Sometimes it sets the wrong key.
    elseif (isset($json[0])) {
      $values = [];
      foreach ($json as $val) {
        if (is_array($val) && isset($val[key($val)])) {
          $values[] = $val[key($val)];
        }
      }
      return $values;
    }
    // Sometimes it does not return with values in GPT 3.5.
    elseif (is_array($json) && isset($json[0][0])) {
      $values = [];
      foreach ($json as $vals) {
        foreach ($vals as $val) {
          if (isset($val)) {
            $values[] = $val;
          }
        }
      }
      return $values;
    }
    elseif (isset($json['value'])) {
      return [$json['value']];
    }
    return [];
  }

  /**
   * Decode structured responses (wrapped "values" format).
   *
   * @param array<mixed> $json
   *   Decoded JSON response.
   *
   * @return array<mixed>
   *   Decoded values.
   */
  protected function decodeStructuredResponse(array $json): array {
    if (isset($json['values']) && is_array($json['values'])) {
      return $this->decodeValueArray($json['values']);
    }
    return $this->decodeValueArray($json);
  }

  /**
   * Generate the tags for the LLM request.
   *
   * This allows event subscribers to identify automator requests and responses
   * and can provide context such as the entity and field the automator is
   * attached to.
   *
   * @param string $prompt
   *   The prompt.
   * @param array<string,mixed> $automatorConfig
   *   The automator configuration.
   * @param \Drupal\ai\OperationType\Chat\ChatInterface $instance
   *   The LLM instance.
   * @param \Drupal\Core\Entity\ContentEntityInterface|null $entity
   *   The entity if available.
   *
   * @return string[]
   *   The array of tags for the LLM request.
   */
  public function getTags(string $prompt, array $automatorConfig, $instance, ?ContentEntityInterface $entity = NULL): array {
    // Always add an automator tag.
    $tags = [
      'ai_automator',
    ];

    // Add the rule being used as a tag.
    if (!empty($automatorConfig['rule'])) {
      $tags[] = 'ai_automator:type:' . $automatorConfig['rule'];
    }

    // Add some tags based on the entity & field name.
    if ($entity) {
      $tags[] = 'ai_automator:entity_type:' . $entity->getEntityTypeId();
      $tags[] = 'ai_automator:entity:' . $entity->id();
      $tags[] = 'ai_automator:bundle:' . $entity->bundle();
    }
    if (!empty($automatorConfig['field_name'])) {
      $tags[] = 'ai_automator:field_name:' . $automatorConfig['field_name'];
    }
    return $tags;
  }

}
