<?php
/**
 * Copyright (C) 2022  Jaap Jansma (jaap.jansma@civicoop.org)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

namespace Civi\FormProcessor;

use Civi\ActionProvider\Action\AbstractAction;
use Civi\ActionProvider\Parameter\ParameterBag;
use Civi\FormProcessor\API\Exception;
use Civi\FormProcessor\Utils\Cache;

class Runner {

  /**
   * @param string $formProcessorName
   * @param array $params
   *
   * @return array
   */
  public static function run($formProcessorName, $params) {
    $container = \Civi::container();
    /*
     * @var \Civi\FormProcessor\DelayedAction\Factory
     */
    $delayedActionFactory = $container->get('form_processor_delayed_action_factory');

    $actionProvider = form_processor_get_action_provider();
    $dataBag = new DataBag();

    $formProcessor = self::getFormProcessor($formProcessorName);

    // Validate the parameters.
    foreach($formProcessor['inputs'] as $input) {
      $inputType = $input['type'];
      unset($input['type']);
      $objInput = new \CRM_FormProcessor_BAO_FormProcessorInput();
      $objInput->copyValues($input);

      // Set default value
      if (!isset($params[$input['name']]) && isset($input['default_value']) && $input['default_value'] != '') {
        $params[$input['name']] = $inputType->getDefaultValue($input['default_value']);
      }

      if ($input['is_required'] && !isset($params[$input['name']])) {
        throw new \API_Exception('Parameter '.$input['name'].' is required');
      }
      if (isset($params[$input['name']]) && !empty($params[$input['name']]) && !$inputType->validateValue($params[$input['name']], $params)) {
        throw new \API_Exception('Parameter '.$input['name'].' is invalid');
      }
      // Check the validations on the input.
      if (isset($params[$input['name']]) && $params[$input['name']] != "") {
        foreach ($input['validators'] as $validator) {
          if (!$validator['validator']->validate($params[$input['name']], $inputType)) {
            throw new \API_Exception($validator['validator']->getInvalidMessage() . ' (Parameter ' . $input['name'] . ')');
          }
        }
      }
      if (isset($params[$objInput->name])) {
        $dataBag->setInputData($objInput, $inputType->normalizeValue($params[$input['name']]), $inputType);
      }
    }

    // Execute the actions
    foreach($formProcessor['actions'] as $action) {
      $actionClass = $actionProvider->getActionByName($action['type']);
      $actionClass->getConfiguration()->fromArray($action['configuration'], $actionClass->getConfigurationSpecification());
      $objAction = new \CRM_FormProcessor_BAO_FormProcessorAction();
      $objAction->copyValues($action);

      // Handle multiple-value parameters.
      $action['mapping'] = self::processMultipleValueParameters($actionClass, $action['mapping']);
      // Create a parameter bag for the action
      $mappedParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['mapping'], $actionProvider);

      // Create a condition class for this action
      if (!is_array($action['condition_configuration'])) {
        $action['condition_configuration'] = array();
      }
      if (!isset($action['condition_configuration']['parameter_mapping']) || !is_array($action['condition_configuration']['parameter_mapping'])) {
        $action['condition_configuration']['parameter_mapping'] = array();
      }
      $mappedConditionParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['condition_configuration']['parameter_mapping'], $actionProvider);
      if (!isset($action['condition_configuration']['output_mapping']) || !is_array($action['condition_configuration']['output_mapping'])) {
        $action['condition_configuration']['output_mapping'] = array();
      }
      $mappedConditionOutputParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['condition_configuration']['output_mapping'], $actionProvider);
      $condition = \CRM_FormProcessor_Condition::getConditionClass($action['condition_configuration']);
      $actionClass->setCondition($condition);

      // Check whether the action should be delayed
      if (isset($action['delay']) && $action['delay']) {
        $delayClass = $delayedActionFactory->getHandlerByName($action['delay']);
        $configuration = $delayClass->getDefaultConfiguration();
        if (is_array($action['delay_configuration'])) {
          foreach ($action['delay_configuration'] as $name => $value) {
            $configuration->set($name, $value);
          }
        }
        $delayClass->setConfiguration($configuration);
        $delayTo = $delayClass->getDelayedTo($dataBag);
        self::queueAction($formProcessor['name'], $action['name'], $mappedParameterBag, $mappedConditionParameterBag, $delayTo);
      } else {
        // There is no delay. Execute the action immediately
        // Now execute the action
        try {
          $outputBag = $actionClass->execute($mappedParameterBag, $mappedConditionParameterBag, $mappedConditionOutputParameterBag);
          // Add the output of the action to the data bag of this action.
          $dataBag->setActionDataFromActionProviderParameterBag($objAction, $outputBag);
        } catch (\Exception $e) {
          throw new Exception($action['title'], $e);
        }
      }
    }

    return $formProcessor['output_handler']->handle($dataBag);
  }

  /**
   * @param string $formProcessorName
   * @param array $params
   *
   * @return array
   * @throws \API_Exception|\CRM_Core_Exception
   */
  public static function runDefaults($formProcessorName, $params) {
    $actionProvider = form_processor_get_action_provider_for_default_data();
    $dataBag = new DataBag();
    $formProcessor = self::getFormProcessor($formProcessorName);
    if (empty($formProcessor['enable_default_data'])) {
      throw new \API_Exception('Default data is not enabled');
    }

    // Validate the parameters.
    foreach ($formProcessor['default_data_inputs'] as $input) {
      $inputType = $input['type'];
      unset($input['type']);
      $objInput = new \CRM_FormProcessor_BAO_FormProcessorDefaultDataInput();
      $objInput->copyValues($input);

      // Set default value
      if (!isset($params[$input['name']]) && isset($input['default_value']) && $input['default_value'] != '') {
        $params[$input['name']] = $input['type']->getDefaultValue($input['default_value']);
      }

      if ($input['is_required'] && !isset($params[$input['name']])) {
        throw new \API_Exception('Parameter ' . $input['name'] . ' is required');
      }
      if (isset($params[$input['name']]) && !empty($params[$input['name']]) && !$inputType->validateValue($params[$input['name']], $params)) {
        throw new \API_Exception('Parameter ' . $input['name'] . ' is invalid');
      }
      // Check the validations on the input.
      if (isset($params[$input['name']]) && $params[$input['name']] != "") {
        foreach ($input['validators'] as $validator) {
          if (!$validator['validator']->validate($params[$input['name']], $inputType)) {
            throw new \API_Exception($validator['validator']->getInvalidMessage() . ' (Parameter ' . $input['name'] . ')');
          }
        }
      }

      $dataBag->setInputData($objInput, $inputType->normalizeValue($params[$input['name']]), $inputType);
    }

    // Execute the actions
    $actionParams = array();
    foreach ($formProcessor['default_data_actions'] as $action) {
      $actionClass = $actionProvider->getActionByName($action['type']);
      $actionClass->getConfiguration()->fromArray($action['configuration']);
      $objAction = new \CRM_FormProcessor_BAO_FormProcessorDefaultDataAction();
      $objAction->copyValues($action);

      // Create a parameter bag for the action
      $mappedParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['mapping'], $actionProvider);

      // Create a condition class for this action
      if (!is_array($action['condition_configuration'])) {
        $action['condition_configuration'] = [];
      }
      if (!isset($action['condition_configuration']['parameter_mapping']) || !is_array($action['condition_configuration']['parameter_mapping'])) {
        $action['condition_configuration']['parameter_mapping'] = [];
      }
      $mappedConditionParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['condition_configuration']['parameter_mapping'], $actionProvider);
      if (!isset($action['condition_configuration']['output_mapping']) || !is_array($action['condition_configuration']['output_mapping'])) {
        $action['condition_configuration']['output_mapping'] = [];
      }
      $mappedConditionOutputParameterBag = self::convertDataBagToMappedParameterBag($dataBag, $action['condition_configuration']['output_mapping'], $actionProvider);
      $condition = \CRM_FormProcessor_Condition::getConditionClass($action['condition_configuration']);
      $actionClass->setCondition($condition);

      try {
        // Now execute the action
        $outputBag = $actionClass->execute($mappedParameterBag, $mappedConditionParameterBag, $mappedConditionOutputParameterBag);
        // Add the output of the action to the data bag of this action.
        $dataBag->setActionDataFromActionProviderParameterBag($objAction, $outputBag);
      } catch (\Exception $e) {
        throw new \API_Exception($action['title'], $e);
      }
    }

    $return = array();
    foreach ($formProcessor['default_data_output_configuration'] as $field => $alias) {
      foreach ($formProcessor['inputs'] as $input) {
        if ($input['name'] == $field) {
          $return[$field] = $input['type']->denormalizeValue($dataBag->getDataByAlias($alias));
        }
      }
    }
    return $return;
  }

  /**
   * Gets the form processor from the cache.
   * @param $name
   *
   * @return \CRM_FormProcessor_BAO_FormProcessorInstance
   * @throws \API_Exception
   * @throws \CRM_Core_Exception
   */
  public static function getFormProcessor($name) {
    $cachekey = 'FormProcessor.run.get.'.$name;
    if (!$formProcessor = Cache::get($cachekey)) {
      // Find the form processor
      $formProcessors = \CRM_FormProcessor_BAO_FormProcessorInstance::getValues(['name' => $name]);
      if (count($formProcessors) != 1) {
        throw new \API_Exception('Could not find a form processor');
      }
      $formProcessor = reset($formProcessors);
      Cache::set($cachekey, $formProcessor);
    }
    return $formProcessor;
  }

  /**
   * Converts a DataBag object to a mapped parameterBag.
   *
   * @param DataBag $dataBag
   * @param array $mapping
   * @param $actionProvider
   * @return ParameterBag;
   */
  protected static function convertDataBagToMappedParameterBag(DataBag $dataBag, $mapping, $actionProvider) {
    $parameterBag = $actionProvider->createParameterBag();
    $fields = $dataBag->getAllAliases();
    foreach($fields as $field) {
      $parameterBag->setParameter($field, $dataBag->getDataByAlias($field));
    }
    $mappedParameterBag = $actionProvider->createdMappedParameterBag($parameterBag, $mapping);
    return $mappedParameterBag;
  }

  /**
   * Save a delayed action into the queue
   *
   * @param $form_processor_name
   * @param $action_name
   * @param $mappedParameterBag
   * @param $mappedConditionParameterBag
   * @param \DateTime $delayedTo
   */
  private static function queueAction($form_processor_name, $action_name, ParameterBag $mappedParameterBag, ParameterBag $mappedConditionParameterBag, \DateTime $delayedTo) {
    $queue = \CRM_Queue_Service::singleton()->create(array(
      'type' => 'FormProcessor',
      'name' => 'FormProcessorDelayedActions',
      'reset' => false, //do not flush queue upon creation
    ));

    //create a task with the action and eventData as parameters
    $task = new \CRM_Queue_Task(
      array('Civi\FormProcessor\API\FormProcessor', 'executeDelayedAction'), //call back method
      array($form_processor_name, $action_name, $mappedParameterBag->toArray(), $mappedConditionParameterBag->toArray()),
      'Delayed Form Processor Action: '.$form_processor_name.'.'.$action_name
    );

    //save the task with a delay
    $dao              = new \CRM_Queue_DAO_QueueItem();
    $dao->queue_name  = $queue->getName();
    $dao->submit_time = \CRM_Utils_Time::getTime('YmdHis');
    $dao->data        = serialize($task);
    $dao->weight      = 0; //weight, normal priority
    $dao->release_time = $delayedTo->format('YmdHis');
    $dao->save();
  }

  /**
   * Convert multiple-value parameter mappings to arrays.
   */
  private static function processMultipleValueParameters(AbstractAction $actionClass, array $actionMapping) : array {
    $parameterSpecificationBag = $actionClass->getParameterSpecification();
    foreach ($actionMapping as $mappedField => $mappedInput) {
      $spec = $parameterSpecificationBag->getSpecificationByName($mappedField);
      if ($spec !== NULL && $spec->isMultiple()) {
        $actionMapping[$mappedField] = explode(',', $mappedInput);
      }
    }
    return $actionMapping;
  }

}
