View source  
  <?php
class AcquiaLiftAPI implements PersonalizeLoggerAwareInterface, AcquiaLiftReportDataSourceInterface {
  const API_URL = 'api.lift.acquia.com';
  const EXPLORATION_RATE_RANDOM = 1;
  const FEATURE_STRING_REPLACE_PATTERN = '[^A-Za-z0-9_-]';
  const FEATURE_STRING_MAX_LENGTH = 50;
  const FEATURE_STRING_SEPARATOR_MUTEX = ':';
  const FEATURE_STRING_SEPARATOR_NONMUTEX = '::';
  const VALID_NAME_MATCH_PATTERN = '[A-Za-z0-9_-]';
  const ENABLED_STATUS = 'enabled';
  const PAUSED_STATUS = 'paused';
  const STOPPED_STATUS = 'stopped';
  const PROVISIONAL_STATUS = 'provisional';
  
  protected $api_key;
  
  protected $admin_key;
  
  protected $owner_code;
  
  protected $api_url;
  
  protected $httpClient;
  
  private static $instance;
  protected $logger = NULL;
  
  public static function getInstance($account_info) {
    if (empty(self::$instance)) {
      if (drupal_valid_test_ua()) {
        self::setTestInstance();
        return self::$instance;
      }
      foreach (array(
        'api_key',
        'admin_key',
        'owner_code',
      ) as $key) {
        if (!isset($account_info[$key])) {
          throw new AcquiaLiftCredsException('Acquia Lift account info is not complete.');
        }
      }
      if (!self::codeIsValid($account_info['owner_code'])) {
        throw new AcquiaLiftCredsException('Acquia Lift owner code is invalid.');
      }
      $api_url = self::API_URL;
      $needs_scheme = TRUE;
      if (!empty($account_info['api_url'])) {
        if (!valid_url($account_info['api_url'])) {
          throw new AcquiaLiftCredsException('Acquia Lift API URL is not a valid URL.');
        }
        $api_url = $account_info['api_url'];
        $needs_scheme = strpos($api_url, '://') === FALSE;
      }
      if ($needs_scheme) {
        global $is_https;
        
        $url_scheme = $is_https ? 'https://' : 'http://';
        $api_url = $url_scheme . $api_url;
      }
      if (substr($api_url, -1) === '/') {
        $api_url = substr($api_url, 0, -1);
      }
      self::$instance = new self($account_info['api_key'], $account_info['admin_key'], $account_info['owner_code'], $api_url);
    }
    return self::$instance;
  }
  
  public static function reset() {
    self::$instance = NULL;
  }
  
  public static function setTestInstance($broken_http_client = FALSE, $simulate_client_side_breakage = FALSE) {
    module_load_include('inc', 'acquia_lift', 'tests/acquia_lift.test_classes');
    self::$instance = new self('test-api-key', 'test-admin-key', 'test-owner-code', 'http://api.example.com');
    
    $test_data = variable_get('acquia_lift_web_test_data', array());
    self::$instance
      ->setHttpClient(new DummyAcquiaLiftHttpClient($broken_http_client, $test_data, $simulate_client_side_breakage));
    self::$instance
      ->setLogger(new AcquiaLiftTestLogger(FALSE));
  }
  
  public static function mapStatus($status) {
    $status_map = array(
      PERSONALIZE_STATUS_NOT_STARTED => self::PAUSED_STATUS,
      PERSONALIZE_STATUS_PAUSED => self::PAUSED_STATUS,
      PERSONALIZE_STATUS_RUNNING => self::ENABLED_STATUS,
      PERSONALIZE_STATUS_COMPLETED => self::STOPPED_STATUS,
    );
    
    if (in_array($status, $status_map)) {
      return $status;
    }
    
    if (!isset($status_map[$status])) {
      throw new AcquiaLiftException('Unknown agent status: ' . $status);
    }
    return $status_map[$status];
  }
  
  public static function isSuccessful($code) {
    return $code >= 200 && $code < 300;
  }
  
  public static function isClientError($code) {
    return $code >= 400 && $code < 500;
  }
  
  public static function isServerError($code) {
    return $code >= 500 && $code < 600;
  }
  
  protected static function mapBadResponseToExceptionClass($code) {
    if (self::isSuccessful($code)) {
      return NULL;
    }
    if (self::isClientError($code)) {
      switch ($code) {
        case 404:
          return 'AcquiaLiftNotFoundException';
        case 403:
          return 'AcquiaLiftForbiddenException';
        default:
          return 'AcquiaLiftClientErrorException';
      }
    }
    elseif (self::isServerError($code)) {
      return 'AcquiaLiftServerErrorException';
    }
    return NULL;
  }
  
  public static function codeIsValid($str) {
    return (bool) preg_match('/^' . self::VALID_NAME_MATCH_PATTERN . '+$/', $str);
  }
  
  public static function cleanFeatureString($str) {
    $string = preg_replace('/' . self::FEATURE_STRING_REPLACE_PATTERN . '/', '-', $str);
    
    $string = preg_replace('/\\-{2,}/', '-', $string);
    return $string;
  }
  
  private function __construct($api_key, $admin_key, $owner_code, $api_url) {
    $this->api_key = $api_key;
    $this->admin_key = $admin_key;
    $this->owner_code = $owner_code;
    $this->api_url = $api_url;
  }
  
  public function getApiKey() {
    return $this->api_key;
  }
  
  public function getAdminKey() {
    return $this->admin_key;
  }
  
  public function getOwnerCode() {
    return $this->owner_code;
  }
  
  public function getApiUrl() {
    return $this->api_url;
  }
  
  protected function httpClient() {
    if (!isset($this->httpClient)) {
      $this->httpClient = new AcquiaLiftDrupalHttpClient();
    }
    return $this->httpClient;
  }
  
  public function setHttpClient(AcquiaLiftDrupalHttpClientInterface $client) {
    $this->httpClient = $client;
  }
  
  protected function generateEndpoint($path, $admin = TRUE) {
    $endpoint = $this->api_url . '/';
    $endpoint .= $this->owner_code;
    if (substr($path, 0, 1) !== '/') {
      $endpoint .= '/';
    }
    $endpoint .= $path;
    
    if (strpos($endpoint, '?')) {
      $endpoint .= '&';
    }
    else {
      $endpoint .= '?';
    }
    $key = $admin ? $this
      ->getAdminKey() : $this
      ->getApiKey();
    $endpoint .= "apikey={$key}";
    return $endpoint;
  }
  
  protected function logger() {
    if ($this->logger !== NULL) {
      return $this->logger;
    }
    return new PersonalizeLogger();
  }
  
  public function setLogger(PersonalizeLoggerInterface $logger) {
    $this->logger = $logger;
  }
  
  public function pingTest() {
    
    $url = $this
      ->generateEndpoint("/list-agents");
    $admin_response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    $url = $this
      ->generateEndpoint('/ping-test-agent/expire', FALSE);
    $runtime_response = $this
      ->httpClient()
      ->post($url, array(
      'Accept' => 'application/json',
    ));
    return $admin_response->code == 200 && $runtime_response->code != 403;
  }
  
  public function saveAgent($machine_name, $label, $decision_style, $status = 'enabled', $control_rate = 0.1, $explore_rate = 0.2, $sticky = TRUE) {
    
    if ($decision_style !== "random") {
      $decision_style = "adaptive";
    }
    $status = self::mapStatus($status);
    $url = $this
      ->generateEndpoint("/agent-api/{$machine_name}");
    $agent = array(
      'name' => $label,
      'selection-mode' => $decision_style,
      'status' => $status,
      'control-rate' => $control_rate,
      'explore-rate' => $explore_rate,
      'decision-stickiness' => $sticky ? 'session' : 'none',
    );
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $agent);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $machine_name,
    );
    $success_msg = 'The campaign {agent} was pushed to Acquia Lift';
    $fail_msg = 'The campaign {agent} could not be pushed to Acquia Lift';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function updateAgentStatus($machine_name, $status) {
    $lift_status_code = self::mapStatus($status);
    $url = $this
      ->generateEndpoint("/agent-api/{$machine_name}");
    $agent = array(
      'status' => $lift_status_code,
    );
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $agent);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $machine_name,
    );
    $success_msg = 'The new status of campaign {agent} was pushed to Acquia Lift';
    $fail_msg = 'The new status of campaign {agent} could not be pushed to Acquia Lift';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function handleBadResponse($response_code, $fail_msg, $vars = array(), $log_failure = TRUE) {
    if ($exception_class = self::mapBadResponseToExceptionClass($response_code)) {
      if ($log_failure) {
        $this
          ->logger()
          ->log(PersonalizeLogLevel::ERROR, $fail_msg, $vars);
      }
      throw new $exception_class(t($fail_msg, $vars));
    }
    if (self::isSuccessful($response_code)) {
      
      if ($log_failure) {
        $this
          ->logger()
          ->log(PersonalizeLogLevel::WARNING, $fail_msg, $vars);
      }
      return;
    }
    
    throw new AcquiaLiftException($fail_msg);
  }
  
  public function savePoint($agent_name, $point_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}");
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ));
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
    );
    $success_msg = 'The point {decpoint} was pushed to the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not save the point {decpoint} to the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function saveDecision($agent_name, $point_name, $decision_name, $data = array()) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions/{$decision_name}");
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $data);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
      'decname' => $decision_name,
    );
    $success_msg = 'The decision {decname} for point {decpoint} was pushed to the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not save decision {decname} for point {decpoint} to the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function saveChoice($agent_name, $point_name, $decision_name, $choice, $data = array()) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions/{$decision_name}/choices/{$choice}");
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $data);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
      'choicename' => $decision_name . ': ' . $choice,
    );
    $success_msg = 'The decision choice {choicename} for point {decpoint} was pushed to the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not save decision choice {choicename} for point {decpoint} to the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function resetAgentData($agent_name) {
    $url = $this
      ->generateEndpoint("/{$agent_name}/data");
    $response = $this
      ->httpClient()
      ->delete($url);
    $vars = array(
      'agent' => $agent_name,
    );
    $success_msg = 'The data for Acquia Lift campaign {agent} was reset';
    $fail_msg = 'Could not reset data for Acquia Lift campaign {agent}';
    if ($response->code == 200) {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deleteAgent($agent_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}");
    $response = $this
      ->httpClient()
      ->delete($url);
    $vars = array(
      'agent' => $agent_name,
    );
    $success_msg = 'The Acquia Lift campaign {agent} was deleted';
    $fail_msg = 'Could not delete Acquia Lift campaign {agent}';
    if ($response->code == 200) {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deletePoint($agent_name, $point_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}");
    $response = $this
      ->httpClient()
      ->delete($url);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
    );
    $success_msg = 'The decision point {decpoint} was deleted from the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not delete decision point {decpoint} from the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deleteDecision($agent_name, $point_name, $decision_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions/{$decision_name}");
    $response = $this
      ->httpClient()
      ->delete($url);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
      'decname' => $decision_name,
    );
    $success_msg = 'The decision {decname} for point {decpoint} was deleted from the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not delete decision {decname} for point {decpoint} from the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deleteChoice($agent_name, $point_name, $decision_name, $choice) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions/{$decision_name}/choices/{$choice}");
    $response = $this
      ->httpClient()
      ->delete($url);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
      'choicename' => $decision_name . ': ' . $choice,
    );
    $success_msg = 'The decision choice {choicename} for point {decpoint} was deleted from the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not delete decision choice {choicename} for point {decpoint} from the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::ERROR, $fail_msg, $vars);
      throw new AcquiaLiftException($fail_msg);
    }
  }
  
  public function saveGoal($agent_name, $goal_name, $data = array()) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/goals/{$goal_name}");
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $data);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'goalname' => $goal_name,
    );
    $success_msg = 'The goal {goalname} was pushed to the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not save the goal {goalname} to the Acquia Lift campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deleteGoal($agent_name, $goal_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/goals/{$goal_name}");
    $response = $this
      ->httpClient()
      ->delete($url);
    $vars = array(
      'agent' => $agent_name,
      'goalname' => $goal_name,
    );
    $success_msg = 'The goal {goalname} was deleted from the Acquia Lift campaign {agent}';
    $fail_msg = 'Could not delete the goal {goalname} from the Acquia Lift campaign {agent}';
    if ($response->code == 200) {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function getAgent($machine_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$machine_name}");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      return json_decode($response->data, TRUE);
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Could not retrieve agent from Acquia Lift', array(), FALSE);
    }
    return FALSE;
  }
  
  public function getGoalsForAgent($agent_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/goals");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      return json_decode($response->data, TRUE);
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Could not retrieve goals from Acquia Lift', array(), FALSE);
    }
    return FALSE;
  }
  
  public function getPointsForAgent($agent_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      return json_decode($response->data, TRUE);
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Could not retrieve decision points from Acquia Lift', array(), FALSE);
    }
    return FALSE;
  }
  
  public function getDecisionsForPoint($agent_name, $point_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      return json_decode($response->data, TRUE);
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Could not retrieve decisions from Acquia Lift', array(), FALSE);
    }
    return FALSE;
  }
  
  public function getChoicesForDecision($agent_name, $point_name, $decision_name) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/decisions/{$decision_name}/choices");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      return json_decode($response->data, TRUE);
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Could not retrieve choices from Acquia Lift', array(), FALSE);
    }
    return FALSE;
  }
  
  public function getExistingAgents() {
    $url = $this
      ->generateEndpoint("/list-agents");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      $response = json_decode($response->data, TRUE);
      if (!isset($response['data']['agents'])) {
        return array();
      }
      $existing_agents = array();
      foreach ($response['data']['agents'] as $agent) {
        $existing_agents[$agent['code']] = $agent;
      }
      return $existing_agents;
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Error retrieving agent list from Acquia Lift');
    }
    return array();
  }
  
  public function getTransformOptions() {
    $url = $this
      ->generateEndpoint("/transforms-options");
    
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code == 200) {
      $response = json_decode($response->data, TRUE);
      return $response['data']['options'];
    }
    else {
      $this
        ->handleBadResponse($response->code, 'Error retrieving list of transforms options');
    }
    return FALSE;
  }
  
  public function saveAutoTargetingRule($agent_name, $auto_features) {
    $rule_name = $agent_name . '-auto-targeting';
    $url = $this
      ->generateEndpoint("/transform-rule");
    foreach ($auto_features as &$feature) {
      $feature = '#' . $feature;
    }
    $body = array(
      'code' => $rule_name,
      'status' => 1,
      'agents' => array(
        $agent_name,
      ),
      'when' => array(),
      'apply' => array(
        'feature' => implode(',', $auto_features),
      ),
    );
    $response = $this
      ->httpClient()
      ->post($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $body);
    $vars = array(
      'agent' => $agent_name,
    );
    $success_msg = 'The targeting rule for campaign {agent} was saved successfully';
    $fail_msg = 'The targeting rule could not be saved for campaign {agent}';
    if ($response->code == 200) {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function deleteAutoTargetingRule($agent_name) {
    $rule_name = $agent_name . '-auto-targeting';
    $url = $this
      ->generateEndpoint("/transform-rule/{$rule_name}");
    $response = $this
      ->httpClient()
      ->delete($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ));
    $vars = array(
      'agent' => $agent_name,
    );
    $success_msg = 'The targeting rule for campaign {agent} was deleted successfully';
    $fail_msg = 'The targeting rule could not be deleted for campaign {agent}';
    if ($response->code == 200) {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function hasAutoTargetingRule($agent_name) {
    $rule_name = $agent_name . '-auto-targeting';
    $url = $this
      ->generateEndpoint("/transforms-list");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving auto-targeting rules');
      return FALSE;
    }
    $response = json_decode($response->data, TRUE);
    if (!isset($response['data']) || !isset($response['data']['transforms'])) {
      return FALSE;
    }
    foreach ($response['data']['transforms'] as $rule) {
      if ($rule['code'] == $rule_name) {
        return TRUE;
      }
    }
    return FALSE;
  }
  
  public function getPotentialTargetingValues($agent_name) {
    $url = $this
      ->generateEndpoint("/-/potential-targeting?agent={$agent_name}&include-current=true");
    $headers = array(
      'Accept' => 'application/json',
    );
    $response = $this
      ->httpClient()
      ->get($url, $headers);
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving potential targeting values');
      return array();
    }
    return json_decode($response->data, TRUE);
  }
  
  public function getPossibleValues($agent_name, $mutex_separator = NULL, $non_mutex_separator = NULL) {
    $possible_values = array();
    
    if (empty($mutex_separator)) {
      $mutex_separator = self::FEATURE_STRING_SEPARATOR_MUTEX;
    }
    if (empty($non_mutex_separator)) {
      $non_mutex_separator = self::FEATURE_STRING_SEPARATOR_NONMUTEX;
    }
    $result = $this
      ->getPotentialTargetingValues($agent_name);
    if (isset($result['data']['potential']['features']) && !empty($result['data']['potential']['features'])) {
      
      $check = $non_mutex_separator;
      if (strpos($mutex_separator, $non_mutex_separator) !== FALSE) {
        
        $check = $mutex_separator;
      }
      foreach ($result['data']['potential']['features'] as $feature) {
        $code = $feature['code'];
        
        $mutex = strpos($code, $check) === FALSE ? $check === $non_mutex_separator : $check === $mutex_separator;
        $separator = $mutex ? $mutex_separator : $non_mutex_separator;
        $separated = explode($separator, $code);
        if (count($separated) !== 2) {
          continue;
        }
        list($name, $value) = $separated;
        $name = trim($name);
        $value = trim($value);
        $friendly_name = isset($feature['typeName']) ? $feature['typeName'] : $name;
        if (!isset($possible_values[$name])) {
          $possible_values[$name] = array(
            'value type' => 'predefined',
            'mutex' => $mutex,
            'friendly name' => $friendly_name,
            'values' => array(),
          );
        }
        $possible_values[$name]['values'][$value] = $feature['name'];
      }
    }
    return $possible_values;
  }
  
  public function saveFixedTargetingMapping($agent_name, $point_name, $map) {
    $url = $this
      ->generateEndpoint("/agent-api/{$agent_name}/points/{$point_name}/fixed-targeting");
    $response = $this
      ->httpClient()
      ->put($url, array(
      'Content-Type' => 'application/json; charset=utf-8',
      'Accept' => 'application/json',
    ), $map);
    $data = json_decode($response->data, TRUE);
    $vars = array(
      'agent' => $agent_name,
      'decpoint' => $point_name,
    );
    $success_msg = 'The fixed targeting mapping for point {decpoint} was successfully saved for campaign {agent}';
    $fail_msg = 'The fixed targeting mapping for point {decpoint} could not be saved for campaign {agent}';
    if ($response->code == 200 && $data['status'] == 'ok') {
      $this
        ->logger()
        ->log(PersonalizeLogLevel::INFO, $success_msg, $vars);
    }
    else {
      $this
        ->handleBadResponse($response->code, $fail_msg, $vars);
    }
  }
  
  public function getTargetingImpactReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL) {
    $date_str = $this
      ->getDateString($date_start, $date_end);
    $url = $this
      ->generateEndpoint("/{$agent_name}/report/targeting-features{$date_str}");
    $headers = array(
      'Accept' => 'application/json',
    );
    if ($point !== NULL) {
      $headers['x-mpath-point'] = $point;
    }
    $response = $this
      ->httpClient()
      ->get($url, $headers);
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving targeting impact report.');
      return array();
    }
    return json_decode($response->data, TRUE);
  }
  
  public function getAgentStatusReport($agent_names, $num_days = NULL) {
    $codes = implode(',', $agent_names);
    $days = is_null($num_days) || !is_numeric($num_days) ? '' : '&days=' . $num_days;
    $url = $this
      ->generateEndpoint("/report/status?codes={$codes}{$days}");
    $response = $this
      ->httpClient()
      ->get($url, array(
      'Accept' => 'application/json',
    ));
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving status report.');
      return array();
    }
    return json_decode($response->data, TRUE);
  }
  
  public function getConfidenceReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL, $options = array()) {
    $default_options = array(
      'confidence-measure' => 0.95,
      'aggregated-over-dates' => true,
      'features' => NULL,
    );
    $options = array_merge($default_options, $options);
    $date_str = $this
      ->getDateString($date_start, $date_end);
    if ($options['features'] === 'all') {
      $features = '';
    }
    else {
      $features = is_array($options['features']) ? implode(',', $options['features']) : "(none)";
    }
    unset($options['features']);
    $url = $this
      ->generateEndpoint("/{$agent_name}/report/confidence{$date_str}?features={$features}");
    foreach ($options as $param => $value) {
      $param_value = is_bool($value) ? var_export($value, TRUE) : (string) $value;
      $url .= "&{$param}=" . $param_value;
    }
    $headers = array(
      'Accept' => 'application/json',
    );
    if ($point !== NULL) {
      $headers['x-mpath-point'] = $point;
    }
    
    $response = $this
      ->httpClient()
      ->get($url, $headers);
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving confidence report.');
      return array();
    }
    return json_decode($response->data, TRUE);
  }
  
  public function getContextFilters($agent_name) {
    return $this
      ->getPotentialTargetingValues($agent_name);
  }
  
  public function getRawLearningReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL) {
    $date_str = $this
      ->getDateString($date_start, $date_end);
    $url = $this
      ->generateEndpoint("/{$agent_name}/report/learning{$date_str}");
    $headers = array(
      'Accept' => 'application/json',
    );
    if ($point !== NULL) {
      $headers['x-mpath-point'] = $point;
    }
    
    $response = $this
      ->httpClient()
      ->get($url, $headers);
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving learning report.');
      return array();
    }
    return json_decode($response->data, TRUE);
  }
  
  public function getAPICallsForPeriod($date_start, $date_end) {
    $date_str = $this
      ->getDateString($date_start, $date_end);
    $url = $this
      ->generateEndpoint("/-/report/system-usage{$date_str}");
    $headers = array(
      'Accept' => 'application/json',
    );
    $response = $this
      ->httpClient()
      ->get($url, $headers);
    if ($response->code != 200) {
      $this
        ->handleBadResponse($response->code, 'Problem retrieving API call counts.');
      return array();
    }
    $result = json_decode($response->data, TRUE);
    if (!isset($result['data']) || !isset($result['data'][0]) || !isset($result['data'][0]['calls'])) {
      return array();
    }
    return $result['data'][0]['calls'];
  }
  
  protected function convertCallCountsToTotalCount($counts, $exclude = array(
    'other',
  )) {
    $total_count = 0;
    foreach ($counts as $type => $count) {
      if (in_array($type, $exclude)) {
        continue;
      }
      $total_count += $count;
    }
    return $total_count;
  }
  
  public function getCallsForPreviousMonth($timestamp) {
    $date = getdate($timestamp);
    $current_month = $date['mon'];
    $current_month_year = $last_month_year = $date['year'];
    if ($current_month == 1) {
      $last_month = 12;
      $last_month_year = $current_month_year - 1;
    }
    else {
      $last_month = $current_month - 1;
      if ($last_month < 10) {
        $last_month = '0' . $last_month;
      }
    }
    
    $ts_last_month = strtotime("{$last_month}/01/{$last_month_year}");
    
    $num_days_last_month = date('t', $ts_last_month);
    $date_start = $last_month_year . '-' . $last_month . '-01';
    $date_end = $last_month_year . '-' . $last_month . '-' . $num_days_last_month;
    $calls_last_month = $this
      ->getAPICallsForPeriod($date_start, $date_end);
    return $calls_last_month;
  }
  
  public function getTotalRuntimeCallsForPreviousMonth($timestamp) {
    $calls_last_month = $this
      ->getCallsForPreviousMonth($timestamp);
    return $this
      ->convertCallCountsToTotalCount($calls_last_month);
  }
  
  public function getCallsForMonthToDate($timestamp) {
    $date_start = date('Y', $timestamp) . '-' . date('m', $timestamp) . '-01';
    $date_end = date('Y-m-d', $timestamp);
    $calls_this_month = $this
      ->getAPICallsForPeriod($date_start, $date_end);
    return $calls_this_month;
  }
  
  public function getTotalRuntimeCallsForMonthToDate($timestamp) {
    $calls_last_month = $this
      ->getCallsForMonthToDate($timestamp);
    return $this
      ->convertCallCountsToTotalCount($calls_last_month);
  }
  
  public function ensureUniqueAgentName($agent_name, $max_length) {
    if ($max_length > self::FEATURE_STRING_MAX_LENGTH) {
      $max_length = self::FEATURE_STRING_MAX_LENGTH;
    }
    $agent_name = substr($agent_name, 0, $max_length);
    $existing = $this
      ->getExistingAgents();
    $index = 0;
    $suffix = '';
    while (in_array($agent_name . $suffix, array_keys($existing))) {
      $suffix = '-' . $index;
      while (strlen($agent_name . $suffix) > $max_length) {
        $agent_name = substr($agent_name, 0, -1);
      }
      $index++;
    }
    return $agent_name . $suffix;
  }
  
  protected function getDateString($date_start, $date_end) {
    if ($date_start === NULL || !preg_match('/\\d{4}\\-\\d{2}\\-\\d{2}/', $date_start)) {
      $date_start = date('Y-m-d');
    }
    $date_str = '/' . $date_start;
    if ($date_end !== NULL && preg_match('/\\d{4}\\-\\d{2}\\-\\d{2}/', $date_end)) {
      $date_str .= '/' . $date_end;
    }
    return $date_str;
  }
}
interface AcquiaLiftReportDataSourceInterface {
  
  public function getConfidenceReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL, $options = array());
  
  public function getTargetingImpactReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL);
  
  public function getAgentStatusReport($agent_names, $num_days = NULL);
  
  public function getRawLearningReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL);
  
  public function getContextFilters($agent_name);
}
class AcquiaLiftReportDataFromFile implements AcquiaLiftReportDataSourceInterface {
  
  protected $fileLocation;
  
  protected $reports;
  public function __construct($file_location, AcquiaLiftReportCacheInterface $cache = NULL) {
    $this->fileLocation = $file_location;
    $this->cache = $cache;
  }
  
  protected function getReport($agent_name, $report_name) {
    
    if ($this->reports) {
      return isset($this->reports[$report_name]) ? $this->reports[$report_name] : array();
    }
    
    if ($this->cache && ($reports = $this->cache
      ->getCachedReports($agent_name))) {
      $this->reports = $reports;
      return isset($reports[$report_name]) ? $reports[$report_name] : array();
    }
    else {
      $file_name = $this->fileLocation;
      if (strpos($file_name, '://') === FALSE) {
        
        global $base_url;
        $file_name = $base_url . $file_name;
      }
      if ($str = file_get_contents($file_name)) {
        $parsed = json_decode($str, TRUE);
        $this->reports = $parsed['reports'];
        if ($this->cache) {
          $this->cache
            ->cacheReports($agent_name, $this->reports);
        }
        return isset($this->reports[$report_name]) ? $this->reports[$report_name] : array();
      }
    }
    return array();
  }
  
  public function getTargetingImpactReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL) {
    return $this
      ->getReport($agent_name, 'targeting-impact');
  }
  
  public function getAgentStatusReport($agent_names, $num_days = NULL) {
    $agent_name = reset($agent_names);
    return $this
      ->getReport($agent_name, 'agent-status');
  }
  
  public function getConfidenceReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL, $options = array()) {
    $report_name = 'confidence';
    if (!empty($options['goal'])) {
      
      $report_name .= '_goal' . rand(1, 2);
    }
    if (isset($options['aggregated-over-dates']) && $options['aggregated-over-dates'] === FALSE) {
      $report_name .= '_detail';
    }
    return $this
      ->getReport($agent_name, $report_name);
  }
  
  public function getRawLearningReport($agent_name, $date_start = NULL, $date_end = NULL, $point = NULL) {
    return $this
      ->getReport($agent_name, 'raw-learning');
  }
  
  public function getContextFilters($agent_name) {
    $context_filters = $this
      ->getReport($agent_name, 'context-filters');
    return !empty($context_filters) ? $context_filters : array(
      'data' => array(
        'potential' => array(
          'features' => array(),
        ),
      ),
    );
  }
}
interface AcquiaLiftReportCacheInterface {
  
  public function getCachedReports($agent_name);
  
  public function cacheReports($agent_name, $data);
}
class AcquiaLiftReportCache implements AcquiaLiftReportCacheInterface {
  
  protected $cache = array();
  
  protected $bin = 'cache_acquia_lift_reports';
  
  public function getCachedReports($agent_name) {
    if (!isset($this->cache[$agent_name])) {
      if ($get = cache_get($agent_name, $this->bin)) {
        $this->cache[$agent_name] = $get->data;
      }
      else {
        $this->cache[$agent_name] = FALSE;
      }
    }
    return $this->cache[$agent_name];
  }
  
  public function cacheReports($agent_name, $data) {
    cache_set($agent_name, $data, $this->bin);
  }
}
interface AcquiaLiftDrupalHttpClientInterface {
  
  public function get($uri = null, $headers = null, array $options = array());
  
  public function put($uri = null, $headers = null, $body = null, array $options = array());
  
  public function post($uri = null, $headers = null, $body = null, array $options = array());
  
  public function delete($uri = null, $headers = null, $body = null, array $options = array());
}
class AcquiaLiftDrupalHttpClient implements AcquiaLiftDrupalHttpClientInterface {
  
  const REQUEST_TIMEOUT_VALUE_GET = 8.0;
  const REQUEST_TIMEOUT_VALUE_DEFAULT = 15.0;
  protected function encodeBody($body) {
    if (is_string($body)) {
      $data = $body;
    }
    else {
      $data = drupal_json_encode($body);
    }
    return $data;
  }
  
  public function get($uri = NULL, $headers = NULL, array $options = array()) {
    $headers = $headers ? $headers : array();
    $options += array(
      'timeout' => self::REQUEST_TIMEOUT_VALUE_GET,
    );
    $options = array(
      'method' => 'GET',
      'headers' => $headers,
    ) + $options;
    return drupal_http_request($uri, $options);
  }
  
  public function put($uri = NULL, $headers = NULL, $body = NULL, array $options = array()) {
    $data = $body === NULL ? NULL : $this
      ->encodeBody($body);
    $headers = $headers ? $headers : array();
    $options += array(
      'timeout' => self::REQUEST_TIMEOUT_VALUE_DEFAULT,
    );
    $options = array(
      'method' => 'PUT',
      'data' => $data,
      'headers' => $headers,
    ) + $options;
    return drupal_http_request($uri, $options);
  }
  
  public function post($uri = NULL, $headers = NULL, $body = NULL, array $options = array()) {
    $data = $body ? $this
      ->encodeBody($body) : NULL;
    $headers = $headers ? $headers : array();
    $options += array(
      'timeout' => self::REQUEST_TIMEOUT_VALUE_DEFAULT,
    );
    $options = array(
      'method' => 'POST',
      'data' => $data,
      'headers' => $headers,
    ) + $options;
    return drupal_http_request($uri, $options);
  }
  
  public function delete($uri = null, $headers = null, $body = null, array $options = array()) {
    $data = $body ? $this
      ->encodeBody($body) : NULL;
    $headers = $headers ? $headers : array();
    $options += array(
      'timeout' => self::REQUEST_TIMEOUT_VALUE_DEFAULT,
    );
    $options = array(
      'method' => 'DELETE',
      'data' => $data,
      'headers' => $headers,
    ) + $options;
    return drupal_http_request($uri, $options);
  }
}
class AcquiaLiftQueue extends SystemQueue {
  public static $items = array();
  public static $retries = array();
  const MAX_RETRIES = 15;
  
  public static function handleFailedItem($item) {
    if (!isset($item->data['agent'])) {
      return;
    }
    personalize_pause_if_running($item->data['agent']);
  }
  
  public function createItem($data) {
    if (isset($data['hash'])) {
      if (in_array($data['hash'], self::$items)) {
        return;
      }
      else {
        self::$items[] = $data['hash'];
        unset($data['hash']);
      }
    }
    parent::createItem($data);
  }
  
  public function releaseItem($item) {
    if (!isset(self::$retries[$item->item_id])) {
      self::$retries[$item->item_id] = 0;
    }
    elseif (self::$retries[$item->item_id] >= self::MAX_RETRIES) {
      self::handleFailedItem($item);
      $this
        ->deleteItem($item);
      return;
    }
    parent::releaseItem($item);
    self::$retries[$item->item_id]++;
  }
  
  public function deleteQueue() {
    parent::deleteQueue();
    self::$items = array();
  }
}
class AcquiaLiftException extends Exception {
}
class AcquiaLiftServerErrorException extends AcquiaLiftException {
}
class AcquiaLiftClientErrorException extends AcquiaLiftException {
}
class AcquiaLiftNotFoundException extends AcquiaLiftClientErrorException {
}
class AcquiaLiftForbiddenException extends AcquiaLiftClientErrorException {
}
class AcquiaLiftCredsException extends AcquiaLiftException {
}