<?php

final class PhabricatorConfigClusterRepositoriesController
  extends PhabricatorConfigController {

  public function handleRequest(AphrontRequest $request) {
    $nav = $this->buildSideNavView();
    $nav->selectFilter('cluster/repositories/');

    $title = pht('Cluster Repository Status');

    $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories');

    $header = id(new PHUIHeaderView())
      ->setHeader($title)
      ->setProfileHeader(true)
      ->addActionLink(
        id(new PHUIButtonView())
          ->setIcon('fa-book')
          ->setHref($doc_href)
          ->setTag('a')
          ->setText(pht('Documentation')));

    $crumbs = $this
      ->buildApplicationCrumbs()
      ->addTextCrumb(pht('Repository Servers'))
      ->setBorder(true);

    $repository_status = $this->buildClusterRepositoryStatus();
    $repository_errors = $this->buildClusterRepositoryErrors();

    $content = id(new PhabricatorConfigPageView())
      ->setHeader($header)
      ->setContent(
        array(
          $repository_status,
          $repository_errors,
        ));

    return $this->newPage()
      ->setTitle($title)
      ->setCrumbs($crumbs)
      ->setNavigation($nav)
      ->appendChild($content)
      ->addClass('white-background');
  }

  private function buildClusterRepositoryStatus() {
    $viewer = $this->getViewer();

    Javelin::initBehavior('phabricator-tooltips');

    $all_services = id(new AlmanacServiceQuery())
      ->setViewer($viewer)
      ->withServiceTypes(
        array(
          AlmanacClusterRepositoryServiceType::SERVICETYPE,
        ))
      ->needBindings(true)
      ->needProperties(true)
      ->execute();
    $all_services = mpull($all_services, null, 'getPHID');

    $all_repositories = id(new PhabricatorRepositoryQuery())
      ->setViewer($viewer)
      ->withTypes(
        array(
          PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
        ))
      ->execute();
    $all_repositories = mpull($all_repositories, null, 'getPHID');

    $all_versions = id(new PhabricatorRepositoryWorkingCopyVersion())
      ->loadAll();

    $all_devices = $this->getDevices($all_services, false);
    $all_active_devices = $this->getDevices($all_services, true);

    $leader_versions = $this->getLeaderVersionsByRepository(
      $all_repositories,
      $all_versions,
      $all_active_devices);

    $push_times = $this->loadLeaderPushTimes($leader_versions);

    $repository_groups = mgroup($all_repositories, 'getAlmanacServicePHID');
    $repository_versions = mgroup($all_versions, 'getRepositoryPHID');

    $rows = array();
    foreach ($all_services as $service) {
      $service_phid = $service->getPHID();

      if ($service->getAlmanacPropertyValue('closed')) {
        $status_icon = 'fa-folder';
        $status_tip = pht('Closed');
      } else {
        $status_icon = 'fa-folder-open green';
        $status_tip = pht('Open');
      }

      $status_icon = id(new PHUIIconView())
        ->setIcon($status_icon)
        ->addSigil('has-tooltip')
        ->setMetadata(
          array(
            'tip' => $status_tip,
          ));

      $devices = idx($all_devices, $service_phid, array());
      $active_devices = idx($all_active_devices, $service_phid, array());

      $device_icon = 'fa-server green';

      $device_label = pht(
        '%s Active',
        phutil_count($active_devices));

      $device_status = array(
        id(new PHUIIconView())->setIcon($device_icon),
        ' ',
        $device_label,
      );

      $repositories = idx($repository_groups, $service_phid, array());

      $repository_status = pht(
        '%s',
        phutil_count($repositories));

      $no_leader = array();
      $full_sync = array();
      $partial_sync = array();
      $no_sync = array();
      $lag = array();

      // Threshold in seconds before we start complaining that repositories
      // are not synchronized when there is only one leader.
      $threshold = phutil_units('5 minutes in seconds');

      $messages = array();

      foreach ($repositories as $repository) {
        $repository_phid = $repository->getPHID();

        $leader_version = idx($leader_versions, $repository_phid);
        if ($leader_version === null) {
          $no_leader[] = $repository;
          $messages[] = pht(
            'Repository %s has an ambiguous leader.',
            $viewer->renderHandle($repository_phid)->render());
          continue;
        }

        $versions = idx($repository_versions, $repository_phid, array());

        // Filter out any versions for devices which are no longer active.
        foreach ($versions as $key => $version) {
          $version_device_phid = $version->getDevicePHID();
          if (empty($active_devices[$version_device_phid])) {
            unset($versions[$key]);
          }
        }

        $leaders = 0;
        foreach ($versions as $version) {
          if ($version->getRepositoryVersion() == $leader_version) {
            $leaders++;
          }
        }

        if ($leaders == count($active_devices)) {
          $full_sync[] = $repository;
        } else {
          $push_epoch = idx($push_times, $repository_phid);
          if ($push_epoch) {
            $duration = (PhabricatorTime::getNow() - $push_epoch);
            $lag[] = $duration;
          } else {
            $duration = null;
          }

          if ($leaders >= 2 || ($duration && ($duration < $threshold))) {
            $partial_sync[] = $repository;
          } else {
            $no_sync[] = $repository;
            if ($push_epoch) {
              $messages[] = pht(
                'Repository %s has unreplicated changes (for %s).',
                $viewer->renderHandle($repository_phid)->render(),
                phutil_format_relative_time($duration));
            } else {
              $messages[] = pht(
                'Repository %s has unreplicated changes.',
                $viewer->renderHandle($repository_phid)->render());
            }
          }

        }
      }

      $with_lag = false;

      if ($no_leader) {
        $replication_icon = 'fa-times red';
        $replication_label = pht('Ambiguous Leader');
      } else if ($no_sync) {
        $replication_icon = 'fa-refresh yellow';
        $replication_label = pht('Unsynchronized');
        $with_lag = true;
      } else if ($partial_sync) {
        $replication_icon = 'fa-refresh green';
        $replication_label = pht('Partial');
        $with_lag = true;
      } else if ($full_sync) {
        $replication_icon = 'fa-check green';
        $replication_label = pht('Synchronized');
      } else {
        $replication_icon = 'fa-times grey';
        $replication_label = pht('No Repositories');
      }

      if ($with_lag && $lag) {
        $lag_status = phutil_format_relative_time(max($lag));
        $lag_status = pht(' (%s)', $lag_status);
      } else {
        $lag_status = null;
      }

      $replication_status = array(
        id(new PHUIIconView())->setIcon($replication_icon),
        ' ',
        $replication_label,
        $lag_status,
      );

      $messages = phutil_implode_html(phutil_tag('br'), $messages);

      $rows[] = array(
        $status_icon,
        $viewer->renderHandle($service->getPHID()),
        $device_status,
        $repository_status,
        $replication_status,
        $messages,
      );
    }

    return id(new AphrontTableView($rows))
      ->setNoDataString(
        pht('No repository cluster services are configured.'))
      ->setHeaders(
        array(
          null,
          pht('Service'),
          pht('Devices'),
          pht('Repos'),
          pht('Sync'),
          pht('Messages'),
        ))
      ->setColumnClasses(
        array(
          null,
          'pri',
          null,
          null,
          null,
          'wide',
        ));
  }

  private function getDevices(
    array $all_services,
    $only_active) {

    $devices = array();
    foreach ($all_services as $service) {
      $map = array();
      foreach ($service->getBindings() as $binding) {
        if ($only_active && $binding->getIsDisabled()) {
          continue;
        }

        $device = $binding->getDevice();
        $device_phid = $device->getPHID();

        $map[$device_phid] = $device;
      }
      $devices[$service->getPHID()] = $map;
    }

    return $devices;
  }

  private function getLeaderVersionsByRepository(
    array $all_repositories,
    array $all_versions,
    array $active_devices) {

    $version_map = mgroup($all_versions, 'getRepositoryPHID');

    $result = array();
    foreach ($all_repositories as $repository_phid => $repository) {
      $service_phid = $repository->getAlmanacServicePHID();
      if (!$service_phid) {
        continue;
      }

      $devices = idx($active_devices, $service_phid);
      if (!$devices) {
        continue;
      }

      $versions = idx($version_map, $repository_phid, array());
      $versions = mpull($versions, null, 'getDevicePHID');
      $versions = array_select_keys($versions, array_keys($devices));
      if (!$versions) {
        continue;
      }

      $leader = (int)max(mpull($versions, 'getRepositoryVersion'));
      $result[$repository_phid] = $leader;
    }

    return $result;
  }

  private function loadLeaderPushTimes(array $leader_versions) {
    $viewer = $this->getViewer();

    if (!$leader_versions) {
      return array();
    }

    $events = id(new PhabricatorRepositoryPushEventQuery())
      ->setViewer($viewer)
      ->withIDs($leader_versions)
      ->execute();
    $events = mpull($events, null, 'getID');

    $result = array();
    foreach ($leader_versions as $key => $version) {
      $event = idx($events, $version);
      if (!$event) {
        continue;
      }

      $result[$key] = $event->getEpoch();
    }

    return $result;
  }


  private function buildClusterRepositoryErrors() {
    $viewer = $this->getViewer();

    $messages = id(new PhabricatorRepositoryStatusMessage())->loadAllWhere(
      'statusCode IN (%Ls)',
      array(
        PhabricatorRepositoryStatusMessage::CODE_ERROR,
      ));

    $repository_ids = mpull($messages, 'getRepositoryID');
    if ($repository_ids) {
      // NOTE: We're bypassing policies when loading repositories because we
      // want to show errors exist even if the viewer can't see the repository.
      // We use handles to describe the repository below, so the viewer won't
      // actually be able to see any particulars if they can't see the
      // repository.
      $repositories = id(new PhabricatorRepositoryQuery())
        ->setViewer(PhabricatorUser::getOmnipotentUser())
        ->withIDs($repository_ids)
        ->execute();
      $repositories = mpull($repositories, null, 'getID');
    }

    $rows = array();
    foreach ($messages as $message) {
      $repository = idx($repositories, $message->getRepositoryID());
      if (!$repository) {
        continue;
      }

      if (!$repository->isTracked()) {
        continue;
      }

      $icon = id(new PHUIIconView())
        ->setIcon('fa-exclamation-triangle red');

      $rows[] = array(
        $icon,
        $viewer->renderHandle($repository->getPHID()),
        phutil_tag(
          'a',
          array(
            'href' => $repository->getPathURI('manage/status/'),
          ),
          $message->getStatusTypeName()),
      );
    }

    return id(new AphrontTableView($rows))
      ->setNoDataString(
        pht('No active repositories have outstanding errors.'))
      ->setHeaders(
        array(
          null,
          pht('Repository'),
          pht('Error'),
        ))
      ->setColumnClasses(
        array(
          null,
          'pri',
          'wide',
        ));
  }

}
