<?php

/**
 * @task update Updating Resources
 * @task command Processing Commands
 * @task activate Activating Resources
 * @task release Releasing Resources
 * @task break Breaking Resources
 * @task destroy Destroying Resources
 */
final class DrydockResourceUpdateWorker extends DrydockWorker {

  protected function doWork() {
    $resource_phid = $this->getTaskDataValue('resourcePHID');

    $hash = PhabricatorHash::digestForIndex($resource_phid);
    $lock_key = 'drydock.resource:'.$hash;

    $lock = PhabricatorGlobalLock::newLock($lock_key)
      ->lock(1);

    try {
      $resource = $this->loadResource($resource_phid);
      $this->handleUpdate($resource);
    } catch (Exception $ex) {
      $lock->unlock();
      $this->flushDrydockTaskQueue();
      throw $ex;
    }

    $lock->unlock();
  }


/* -(  Updating Resources  )------------------------------------------------- */


  /**
   * Update a resource, handling exceptions thrown during the update.
   *
   * @param DrydockReosource Resource to update.
   * @return void
   * @task update
   */
  private function handleUpdate(DrydockResource $resource) {
    try {
      $this->updateResource($resource);
    } catch (Exception $ex) {
      if ($this->isTemporaryException($ex)) {
        $this->yieldResource($resource, $ex);
      } else {
        $this->breakResource($resource, $ex);
      }
    }
  }


  /**
   * Update a resource.
   *
   * @param DrydockResource Resource to update.
   * @return void
   * @task update
   */
  private function updateResource(DrydockResource $resource) {
    $this->processResourceCommands($resource);

    $resource_status = $resource->getStatus();
    switch ($resource_status) {
      case DrydockResourceStatus::STATUS_PENDING:
        $this->activateResource($resource);
        break;
      case DrydockResourceStatus::STATUS_ACTIVE:
        // Nothing to do.
        break;
      case DrydockResourceStatus::STATUS_RELEASED:
      case DrydockResourceStatus::STATUS_BROKEN:
        $this->destroyResource($resource);
        break;
      case DrydockResourceStatus::STATUS_DESTROYED:
        // Nothing to do.
        break;
    }

    $this->yieldIfExpiringResource($resource);
  }


  /**
   * Convert a temporary exception into a yield.
   *
   * @param DrydockResource Resource to yield.
   * @param Exception Temporary exception worker encountered.
   * @task update
   */
  private function yieldResource(DrydockResource $resource, Exception $ex) {
    $duration = $this->getYieldDurationFromException($ex);

    $resource->logEvent(
      DrydockResourceActivationYieldLogType::LOGCONST,
      array(
        'duration' => $duration,
      ));

    throw new PhabricatorWorkerYieldException($duration);
  }


/* -(  Processing Commands  )------------------------------------------------ */


  /**
   * @task command
   */
  private function processResourceCommands(DrydockResource $resource) {
    if (!$resource->canReceiveCommands()) {
      return;
    }

    $this->checkResourceExpiration($resource);

    $commands = $this->loadCommands($resource->getPHID());
    foreach ($commands as $command) {
      if (!$resource->canReceiveCommands()) {
        break;
      }

      $this->processResourceCommand($resource, $command);

      $command
        ->setIsConsumed(true)
        ->save();
    }
  }


  /**
   * @task command
   */
  private function processResourceCommand(
    DrydockResource $resource,
    DrydockCommand $command) {

    switch ($command->getCommand()) {
      case DrydockCommand::COMMAND_RELEASE:
        $this->releaseResource($resource, null);
        break;
      case DrydockCommand::COMMAND_RECLAIM:
        $reclaimer_phid = $command->getAuthorPHID();
        $this->releaseResource($resource, $reclaimer_phid);
        break;
    }

    // If the command specifies that other worker tasks should be awakened
    // after it executes, awaken them now.
    $awaken_ids = $command->getProperty('awakenTaskIDs');
    if (is_array($awaken_ids) && $awaken_ids) {
      PhabricatorWorker::awakenTaskIDs($awaken_ids);
    }
  }


/* -(  Activating Resources  )----------------------------------------------- */


  /**
   * @task activate
   */
  private function activateResource(DrydockResource $resource) {
    $blueprint = $resource->getBlueprint();
    $blueprint->activateResource($resource);
    $this->validateActivatedResource($blueprint, $resource);

    $awaken_ids = $this->getTaskDataValue('awakenOnActivation');
    if (is_array($awaken_ids) && $awaken_ids) {
      PhabricatorWorker::awakenTaskIDs($awaken_ids);
    }
  }


  /**
   * @task activate
   */
  private function validateActivatedResource(
    DrydockBlueprint $blueprint,
    DrydockResource $resource) {

    if (!$resource->isActivatedResource()) {
      throw new Exception(
        pht(
          'Blueprint "%s" (of type "%s") is not properly implemented: %s '.
          'must actually allocate the resource it returns.',
          $blueprint->getBlueprintName(),
          $blueprint->getClassName(),
          'allocateResource()'));
    }

  }


/* -(  Releasing Resources  )------------------------------------------------ */


  /**
   * @task release
   */
  private function releaseResource(
    DrydockResource $resource,
    $reclaimer_phid) {

    if ($reclaimer_phid) {
      if (!$this->canReclaimResource($resource)) {
        return;
      }

      $resource->logEvent(
        DrydockResourceReclaimLogType::LOGCONST,
        array(
          'reclaimerPHID' => $reclaimer_phid,
        ));
    }

    $viewer = $this->getViewer();
    $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();

    $resource
      ->setStatus(DrydockResourceStatus::STATUS_RELEASED)
      ->save();

    $statuses = array(
      DrydockLeaseStatus::STATUS_PENDING,
      DrydockLeaseStatus::STATUS_ACQUIRED,
      DrydockLeaseStatus::STATUS_ACTIVE,
    );

    $leases = id(new DrydockLeaseQuery())
      ->setViewer($viewer)
      ->withResourcePHIDs(array($resource->getPHID()))
      ->withStatuses($statuses)
      ->execute();

    foreach ($leases as $lease) {
      $command = DrydockCommand::initializeNewCommand($viewer)
        ->setTargetPHID($lease->getPHID())
        ->setAuthorPHID($drydock_phid)
        ->setCommand(DrydockCommand::COMMAND_RELEASE)
        ->save();

      $lease->scheduleUpdate();
    }

    $this->destroyResource($resource);
  }


/* -(  Breaking Resources  )------------------------------------------------- */


  /**
   * @task break
   */
  private function breakResource(DrydockResource $resource, Exception $ex) {
    switch ($resource->getStatus()) {
      case DrydockResourceStatus::STATUS_BROKEN:
      case DrydockResourceStatus::STATUS_RELEASED:
      case DrydockResourceStatus::STATUS_DESTROYED:
        // If the resource was already broken, just throw a normal exception.
        // This will retry the task eventually.
        throw new PhutilProxyException(
          pht(
            'Unexpected failure while destroying resource ("%s").',
            $resource->getPHID()),
          $ex);
    }

    $resource
      ->setStatus(DrydockResourceStatus::STATUS_BROKEN)
      ->save();

    $resource->scheduleUpdate();

    $resource->logEvent(
      DrydockResourceActivationFailureLogType::LOGCONST,
      array(
        'class' => get_class($ex),
        'message' => $ex->getMessage(),
      ));

    throw new PhabricatorWorkerPermanentFailureException(
      pht(
        'Permanent failure while activating resource ("%s"): %s',
        $resource->getPHID(),
        $ex->getMessage()));
  }


/* -(  Destroying Resources  )----------------------------------------------- */


  /**
   * @task destroy
   */
  private function destroyResource(DrydockResource $resource) {
    $blueprint = $resource->getBlueprint();
    $blueprint->destroyResource($resource);

    DrydockSlotLock::releaseLocks($resource->getPHID());

    $resource
      ->setStatus(DrydockResourceStatus::STATUS_DESTROYED)
      ->save();
  }
}
