<?php

final class DiffusionCommitAuditorsTransaction
  extends DiffusionCommitTransactionType {

  const TRANSACTIONTYPE = 'diffusion.commit.auditors';

  public function generateOldValue($object) {
    $auditors = $object->getAudits();
    return mpull($auditors, 'getAuditStatus', 'getAuditorPHID');
  }

  public function generateNewValue($object, $value) {
    $actor = $this->getActor();

    $auditors = $this->generateOldValue($object);
    $old_auditors = $auditors;

    $request_status = PhabricatorAuditRequestStatus::AUDIT_REQUESTED;

    $rem = idx($value, '-', array());
    foreach ($rem as $phid) {
      unset($auditors[$phid]);
    }

    $add = idx($value, '+', array());
    $add_map = array();
    foreach ($add as $phid) {
      $add_map[$phid] = $request_status;
    }

    $set = idx($value, '=', null);
    if ($set !== null) {
      foreach ($set as $phid) {
        $add_map[$phid] = $request_status;
      }

      $auditors = array();
    }

    foreach ($add_map as $phid => $new_status) {
      $old_status = idx($old_auditors, $phid);

      if ($old_status) {
        $auditors[$phid] = $old_status;
        continue;
      }

      $auditors[$phid] = $new_status;
    }

    return $auditors;
  }

  public function getTransactionHasEffect($object, $old, $new) {
    ksort($old);
    ksort($new);
    return ($old !== $new);
  }

  public function mergeTransactions(
    $object,
    PhabricatorApplicationTransaction $u,
    PhabricatorApplicationTransaction $v) {

    $u_new = $u->getNewValue();
    $v_new = $v->getNewValue();

    $result = $v_new;
    foreach (array('-', '+') as $key) {
      $u_value = idx($u_new, $key, array());
      $v_value = idx($v_new, $key, array());

      $merged = $u_value + $v_value;

      if ($merged) {
        $result[$key] = $merged;
      }
    }

    $u->setNewValue($result);

    return $u;
  }

  public function applyExternalEffects($object, $value) {
    $src_phid = $object->getPHID();

    $old = $this->generateOldValue($object);
    $new = $value;

    $auditors = $object->getAudits();
    $auditors = mpull($auditors, null, 'getAuditorPHID');

    $rem = array_diff_key($old, $new);
    foreach ($rem as $phid => $status) {
      $auditor = idx($auditors, $phid);
      if ($auditor) {
        $auditor->delete();
      }
    }

    $this->updateAudits($object, $new);
  }

  public function getTitle() {
    $old = $this->getOldValue();
    $new = $this->getNewValue();

    $rem = array_diff_key($old, $new);
    $add = array_diff_key($new, $old);
    $rem_phids = array_keys($rem);
    $add_phids = array_keys($add);
    $total_count = count($rem) + count($add);

    if ($rem && $add) {
      return pht(
        '%s edited %s auditor(s), removed %s: %s; added %s: %s.',
        $this->renderAuthor(),
        new PhutilNumber($total_count),
        phutil_count($rem_phids),
        $this->renderHandleList($rem_phids),
        phutil_count($add_phids),
        $this->renderHandleList($add_phids));
    } else if ($add) {
      return pht(
        '%s added %s auditor(s): %s.',
        $this->renderAuthor(),
        phutil_count($add_phids),
        $this->renderHandleList($add_phids));
    } else {
      return pht(
        '%s removed %s auditor(s): %s.',
        $this->renderAuthor(),
        phutil_count($rem_phids),
        $this->renderHandleList($rem_phids));
    }
  }

  public function getTitleForFeed() {
    $old = $this->getOldValue();
    $new = $this->getNewValue();

    $rem = array_diff_key($old, $new);
    $add = array_diff_key($new, $old);
    $rem_phids = array_keys($rem);
    $add_phids = array_keys($add);
    $total_count = count($rem) + count($add);

    if ($rem && $add) {
      return pht(
        '%s edited %s auditor(s) for %s, removed %s: %s; added %s: %s.',
        $this->renderAuthor(),
        new PhutilNumber($total_count),
        $this->renderObject(),
        phutil_count($rem_phids),
        $this->renderHandleList($rem_phids),
        phutil_count($add_phids),
        $this->renderHandleList($add_phids));
    } else if ($add) {
      return pht(
        '%s added %s auditor(s) for %s: %s.',
        $this->renderAuthor(),
        phutil_count($add_phids),
        $this->renderObject(),
        $this->renderHandleList($add_phids));
    } else {
      return pht(
        '%s removed %s auditor(s) for %s: %s.',
        $this->renderAuthor(),
        phutil_count($rem_phids),
        $this->renderObject(),
        $this->renderHandleList($rem_phids));
    }
  }

  public function validateTransactions($object, array $xactions) {
    $actor = $this->getActor();
    $errors = array();

    if (!$xactions) {
      return $errors;
    }

    $author_phid = $object->getEffectiveAuthorPHID();
    $can_author_close_key = 'audit.can-author-close-audit';
    $can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key);

    $old = $this->generateOldValue($object);
    foreach ($xactions as $xaction) {
      $new = $this->generateNewValue($object, $xaction->getNewValue());

      $add = array_diff_key($new, $old);
      if (!$add) {
        continue;
      }

      $objects = id(new PhabricatorObjectQuery())
        ->setViewer($actor)
        ->withPHIDs(array_keys($add))
        ->execute();
      $objects = mpull($objects, null, 'getPHID');

      foreach ($add as $phid => $status) {
        if (!isset($objects[$phid])) {
          $errors[] = $this->newInvalidError(
            pht(
              'Auditor "%s" is not a valid object.',
              $phid),
            $xaction);
          continue;
        }

        switch (phid_get_type($phid)) {
          case PhabricatorPeopleUserPHIDType::TYPECONST:
          case PhabricatorOwnersPackagePHIDType::TYPECONST:
          case PhabricatorProjectProjectPHIDType::TYPECONST:
            break;
          default:
            $errors[] = $this->newInvalidError(
              pht(
                'Auditor "%s" must be a user, a package, or a project.',
                $phid),
              $xaction);
            continue 2;
        }

        $is_self = ($phid === $author_phid);
        if ($is_self && !$can_author_close) {
          $errors[] = $this->newInvalidError(
            pht('The author of a commit can not be an auditor.'),
            $xaction);
          continue;
        }
      }
    }

    return $errors;
  }

}
