<?php

final class PhageRemoteWorkflow
  extends PhageWorkflow {

  protected function didConstruct() {
    $this
      ->setName('remote')
      ->setExamples('**remote** --hosts __hosts__ [__options__] -- __command__')
      ->setSynopsis(pht('Run a "bin/remote" command on a group of hosts.'))
      ->setArguments(
        array(
          array(
            'name' => 'hosts',
            'param' => 'hosts',
            'help' => pht('Run on hosts.'),
          ),
          array(
            'name' => 'pools',
            'param' => 'pools',
            'help' => pht('Run on pools.'),
          ),
          array(
            'name' => 'limit',
            'param' => 'count',
            'help' => pht('Limit parallelism.'),
          ),
          array(
            'name' => 'throttle',
            'param' => 'seconds',
            'help' => pht('Wait this many seconds between commands.'),
          ),
          array(
            'name' => 'args',
            'wildcard' => true,
            'help' => pht('Arguments to pass to "bin/remote".'),
          ),
          array(
            'name' => 'timeout',
            'param' => 'seconds',
            'help' => pht('Command timeout in seconds.'),
          ),
        ));
  }

  public function execute(PhutilArgumentParser $args) {
    $hosts = $args->getArg('hosts');
    $pools = $args->getArg('pools');

    if (!strlen($hosts) && !strlen($pools)) {
      throw new PhutilArgumentUsageException(
        pht(
          'Provide a list of hosts to execute on with "--hosts", or a '.
          'list of host pools with "--pools".'));
    }

    $remote_args = $args->getArg('args');
    if (!$remote_args) {
      throw new PhutilArgumentUsageException(
        pht('Provide a remote command to execute.'));
    }

    $limit = $args->getArg('limit');
    $throttle = $args->getArg('throttle');
    $timeout = $args->getArg('timeout');

    $hosts = $this->expandHosts($hosts, $pools);

    $plan = new PhagePlanAction();

    $local = new PhageLocalAction();

    if ($limit) {
      $local->setLimit($limit);
    }

    if ($throttle) {
      $local->setThrottle($throttle);
    }

    $plan->addAction($local);
    $bin_remote = PhacilityCore::getCorePath('bin/remote');

    $commands = array();
    foreach ($hosts as $host) {
      $host_args = $remote_args;
      array_splice($host_args, 1, 0, array($host));

      $command = csprintf(
        '%R %Ls',
        $bin_remote,
        $host_args);

      $execute = id(new PhageExecuteAction())
        ->setLabel($host)
        ->setCommand($command);

      if ($timeout) {
        $execute->setTimeout($timeout);
      }

      $commands[] = $execute;

      $local->addAction($execute);
    }

    $t_start = microtime(true);
    $plan->executePlan();
    $t_end = microtime(true);

    $done = pht('DONE');
    echo tsprintf(
      "\n-<  %s  >%s\n\n",
      $done,
      str_repeat('-', 80 - (5 + strlen($done))));

    $okay_count = 0;
    foreach ($commands as $command) {
      $exit_code = $command->getExitCode();

      if ($exit_code !== 0) {
        echo tsprintf(
          "**<bg:red> [%s] </bg>** %s\n",
          $command->getLabel(),
          pht(
            'Command failure (%d).',
            $exit_code));
      } else {
        $okay_count++;
      }
    }

    if ($okay_count === count($commands)) {
      echo tsprintf(
        "**<bg:green> %s </bg>** %s\n",
        pht('COMPLETE'),
        pht(
          'Everything went according to plan (in %sms).',
          new PhutilNumber(1000 * ($t_end - $t_start))));
    }

  }

  private function expandHosts($spec, $pools) {
    $parts = preg_split('/[, ]+/', $spec);
    $parts = array_filter($parts);

    $hosts = array();
    foreach ($parts as $part) {
      $matches = null;
      $ok = preg_match_all('/(\d+-\d+)/', $part, $matches, PREG_OFFSET_CAPTURE);

      // If there's nothing like "001-12" in the specification, just use the
      // raw host as provided.
      if (!$ok) {
        $hosts[] = $part;
        continue;
      }

      if (count($matches[1]) > 1) {
        throw new Exception(
          pht(
            'Host specification "%s" is ambiguous.',
            $part));
      }

      $match = $matches[1][0][0];
      $offset = $matches[1][0][1];

      $range = explode('-', $match, 2);
      $width = strlen($range[0]);
      $min = (int)$range[0];
      $max = (int)$range[1];

      if ($min > $max) {
        throw new Exception(
          pht(
            'Host range "%s" is invalid: minimum is larger than maximum.',
            $match));
      }

      if (strlen($max) > $width) {
        throw new Exception(
          pht(
            'Host range "%s" is invalid: range start does not have enough '.
            'leading zeroes to contain the entire range.',
            $match));
      }

      $values = range($min, $max);
      foreach ($values as $value) {
        $value = sprintf("%0{$width}d", $value);
        $host = substr_replace($part, $value, $offset, strlen($match));
        $hosts[] = $host;
      }
    }

    if (strlen($pools)) {
      $bin_remote = PhacilityCore::getCorePath('bin/remote');

      list($stdout) = execx(
        '%R list-hosts %R',
        $bin_remote,

        // NOTE: This is a dummy argument, but "bin/remote" currently requires
        // a host target even if we're just going directly to the bastion. This
        // could be cleaned up at some point.
        'bastion-external.phacility.net');

      $pool_hosts = phutil_json_decode($stdout);

      $prefixes = preg_split('/[, ]+/', $pools);
      $prefixes = array_fill_keys($prefixes, array());
      foreach ($pool_hosts as $pool_host) {
        $host = $pool_host['host'];

        foreach ($prefixes as $prefix => $host_list) {
          if (preg_match('(^'.preg_quote($prefix).'\d)', $host)) {
            $prefixes[$prefix][] = $host;
          }
        }
      }

      foreach ($prefixes as $prefix => $host_list) {
        if (!$host_list) {
          throw new Exception(
            pht(
              'Pool "%s" matched no hosts. Use real pools which contain '.
              'actual hosts.',
              $prefix));
        }

        foreach ($host_list as $host) {
          $hosts[] = $host;
        }
      }
    }

    return $hosts;
  }

}