<?php

final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {

  const PAYPAL_API_USERNAME   = 'paypal.api-username';
  const PAYPAL_API_PASSWORD   = 'paypal.api-password';
  const PAYPAL_API_SIGNATURE  = 'paypal.api-signature';
  const PAYPAL_MODE           = 'paypal.mode';

  public function isAcceptingLivePayments() {
    $mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE);
    return ($mode === 'live');
  }

  public function getName() {
    return pht('PayPal');
  }

  public function getConfigureName() {
    return pht('Add PayPal Payments Account');
  }

  public function getConfigureDescription() {
    return pht(
      'Allows you to accept various payment instruments with a paypal.com '.
      'account.');
  }

  public function getConfigureProvidesDescription() {
    return pht('This merchant accepts payments via PayPal.');
  }

  public function getConfigureInstructions() {
    return pht(
      "To configure PayPal, register or log into an existing account on ".
      "[[https://paypal.com | paypal.com]] (for live payments) or ".
      "[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ".
      "payments). Once logged in:\n\n".
      "  - Navigate to {nav Tools > API Access}.\n".
      "  - Choose **View API Signature**.\n".
      "  - Copy the **API Username**, **API Password** and **Signature** ".
      "    into the fields above.\n\n".
      "You can select whether the provider operates in test mode or ".
      "accepts live payments using the **Mode** dropdown above.\n\n".
      "You can either use `sandbox.paypal.com` to retrieve live credentials, ".
      "or `paypal.com` to retrieve live credentials.");
  }

  public function getAllConfigurableProperties() {
    return array(
      self::PAYPAL_API_USERNAME,
      self::PAYPAL_API_PASSWORD,
      self::PAYPAL_API_SIGNATURE,
      self::PAYPAL_MODE,
    );
  }

  public function getAllConfigurableSecretProperties() {
    return array(
      self::PAYPAL_API_PASSWORD,
      self::PAYPAL_API_SIGNATURE,
    );
  }

  public function processEditForm(
    AphrontRequest $request,
    array $values) {

    $errors = array();
    $issues = array();

    if (!strlen($values[self::PAYPAL_API_USERNAME])) {
      $errors[] = pht('PayPal API Username is required.');
      $issues[self::PAYPAL_API_USERNAME] = pht('Required');
    }

    if (!strlen($values[self::PAYPAL_API_PASSWORD])) {
      $errors[] = pht('PayPal API Password is required.');
      $issues[self::PAYPAL_API_PASSWORD] = pht('Required');
    }

    if (!strlen($values[self::PAYPAL_API_SIGNATURE])) {
      $errors[] = pht('PayPal API Signature is required.');
      $issues[self::PAYPAL_API_SIGNATURE] = pht('Required');
    }

    if (!strlen($values[self::PAYPAL_MODE])) {
      $errors[] = pht('Mode is required.');
      $issues[self::PAYPAL_MODE] = pht('Required');
    }

    return array($errors, $issues, $values);
  }

  public function extendEditForm(
    AphrontRequest $request,
    AphrontFormView $form,
    array $values,
    array $issues) {

    $form
      ->appendChild(
        id(new AphrontFormTextControl())
          ->setName(self::PAYPAL_API_USERNAME)
          ->setValue($values[self::PAYPAL_API_USERNAME])
          ->setError(idx($issues, self::PAYPAL_API_USERNAME, true))
          ->setLabel(pht('Paypal API Username')))
      ->appendChild(
        id(new AphrontFormTextControl())
          ->setName(self::PAYPAL_API_PASSWORD)
          ->setValue($values[self::PAYPAL_API_PASSWORD])
          ->setError(idx($issues, self::PAYPAL_API_PASSWORD, true))
          ->setLabel(pht('Paypal API Password')))
      ->appendChild(
        id(new AphrontFormTextControl())
          ->setName(self::PAYPAL_API_SIGNATURE)
          ->setValue($values[self::PAYPAL_API_SIGNATURE])
          ->setError(idx($issues, self::PAYPAL_API_SIGNATURE, true))
          ->setLabel(pht('Paypal API Signature')))
      ->appendChild(
        id(new AphrontFormSelectControl())
          ->setName(self::PAYPAL_MODE)
          ->setValue($values[self::PAYPAL_MODE])
          ->setError(idx($issues, self::PAYPAL_MODE))
          ->setLabel(pht('Mode'))
          ->setOptions(
            array(
              'test' => pht('Test Mode'),
              'live' => pht('Live Mode'),
            )));

    return;
  }

  public function canRunConfigurationTest() {
    return true;
  }

  public function runConfigurationTest() {
    $result = $this
      ->newPaypalAPICall()
      ->setRawPayPalQuery('GetBalance', array())
      ->resolve();
  }

  public function getPaymentMethodDescription() {
    return pht('Credit Card or PayPal Account');
  }

  public function getPaymentMethodIcon() {
    return 'PayPal';
  }

  public function getPaymentMethodProviderDescription() {
    return 'PayPal';
  }

  protected function executeCharge(
    PhortunePaymentMethod $payment_method,
    PhortuneCharge $charge) {
    throw new Exception('!');
  }

  protected function executeRefund(
    PhortuneCharge $charge,
    PhortuneCharge $refund) {

    $transaction_id = $charge->getMetadataValue('paypal.transactionID');
    if (!$transaction_id) {
      throw new Exception(pht('Charge has no transaction ID!'));
    }

    $refund_amount = $refund->getAmountAsCurrency()->negate();
    $refund_currency = $refund_amount->getCurrency();
    $refund_value = $refund_amount->formatBareValue();

    $params = array(
      'TRANSACTIONID' => $transaction_id,
      'REFUNDTYPE' => 'Partial',
      'AMT' => $refund_value,
      'CURRENCYCODE' => $refund_currency,
    );

    $result = $this
      ->newPaypalAPICall()
      ->setRawPayPalQuery('RefundTransaction', $params)
      ->resolve();

    $charge->setMetadataValue(
      'paypal.refundID',
      $result['REFUNDTRANSACTIONID']);
  }

  public function updateCharge(PhortuneCharge $charge) {
    $transaction_id = $charge->getMetadataValue('paypal.transactionID');
    if (!$transaction_id) {
      throw new Exception(pht('Charge has no transaction ID!'));
    }

    $params = array(
      'TRANSACTIONID' => $transaction_id,
    );

    $result = $this
      ->newPaypalAPICall()
      ->setRawPayPalQuery('GetTransactionDetails', $params)
      ->resolve();

    $is_charge = false;
    $is_fail = false;
    switch ($result['PAYMENTSTATUS']) {
      case 'Processed':
      case 'Completed':
      case 'Completed-Funds-Held':
        $is_charge = true;
        break;
      case 'Partially-Refunded':
      case 'Refunded':
      case 'Reversed':
      case 'Canceled-Reversal':
        // TODO: Handle these.
        return;
      case 'In-Progress':
      case 'Pending':
        // TODO: Also handle these better?
        return;
      case 'Denied':
      case 'Expired':
      case 'Failed':
      case 'None':
      case 'Voided':
      default:
        $is_fail = true;
        break;
    }

    if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) {
      $cart = $charge->getCart();

      $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
        if ($is_charge) {
          $cart->didApplyCharge($charge);
        } else if ($is_fail) {
          $cart->didFailCharge($charge);
        }
      unset($unguarded);
    }
  }

  private function getPaypalAPIUsername() {
    return $this
      ->getProviderConfig()
      ->getMetadataValue(self::PAYPAL_API_USERNAME);
  }

  private function getPaypalAPIPassword() {
    return $this
      ->getProviderConfig()
      ->getMetadataValue(self::PAYPAL_API_PASSWORD);
  }

  private function getPaypalAPISignature() {
    return $this
      ->getProviderConfig()
      ->getMetadataValue(self::PAYPAL_API_SIGNATURE);
  }

/* -(  One-Time Payments  )-------------------------------------------------- */

  public function canProcessOneTimePayments() {
    return true;
  }

/* -(  Controllers  )-------------------------------------------------------- */


  public function canRespondToControllerAction($action) {
    switch ($action) {
      case 'checkout':
      case 'charge':
      case 'cancel':
        return true;
    }
    return parent::canRespondToControllerAction();
  }

  public function processControllerRequest(
    PhortuneProviderActionController $controller,
    AphrontRequest $request) {

    $viewer = $request->getUser();

    $cart = $controller->loadCart($request->getInt('cartID'));
    if (!$cart) {
      return new Aphront404Response();
    }

    $charge = $controller->loadActiveCharge($cart);
    switch ($controller->getAction()) {
      case 'checkout':
        if ($charge) {
          throw new Exception(pht('Cart is already charging!'));
        }
        break;
      case 'charge':
      case 'cancel':
        if (!$charge) {
          throw new Exception(pht('Cart is not charging yet!'));
        }
        break;
    }

    switch ($controller->getAction()) {
      case 'checkout':
        $return_uri = $this->getControllerURI(
          'charge',
          array(
            'cartID' => $cart->getID(),
          ));

        $cancel_uri = $this->getControllerURI(
          'cancel',
          array(
            'cartID' => $cart->getID(),
          ));

        $price = $cart->getTotalPriceAsCurrency();

        $charge = $cart->willApplyCharge($viewer, $this);

        $params = array(
          'PAYMENTREQUEST_0_AMT'            => $price->formatBareValue(),
          'PAYMENTREQUEST_0_CURRENCYCODE'   => $price->getCurrency(),
          'PAYMENTREQUEST_0_PAYMENTACTION'  => 'Sale',
          'PAYMENTREQUEST_0_CUSTOM'         => $charge->getPHID(),
          'PAYMENTREQUEST_0_DESC'           => $cart->getName(),

          'RETURNURL'                       => $return_uri,
          'CANCELURL'                       => $cancel_uri,

          // TODO: This should be cart-dependent if we eventually support
          // physical goods.
          'NOSHIPPING'                      => '1',
        );

        $result = $this
          ->newPaypalAPICall()
          ->setRawPayPalQuery('SetExpressCheckout', $params)
          ->resolve();

        $params = array(
          'cmd'   => '_express-checkout',
          'token' => $result['TOKEN'],
        );

        $uri = new PhutilURI(
          'https://www.sandbox.paypal.com/cgi-bin/webscr',
          $params);

        $cart->setMetadataValue('provider.checkoutURI', (string)$uri);
        $cart->save();

        $charge->setMetadataValue('paypal.token', $result['TOKEN']);
        $charge->save();

        return id(new AphrontRedirectResponse())
          ->setIsExternal(true)
          ->setURI($uri);
      case 'charge':
        if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
          return id(new AphrontRedirectResponse())
            ->setURI($cart->getCheckoutURI());
        }

        $token = $request->getStr('token');

        $params = array(
          'TOKEN' => $token,
        );

        $result = $this
          ->newPaypalAPICall()
          ->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
          ->resolve();

        if ($result['CUSTOM'] !== $charge->getPHID()) {
          throw new Exception(
            pht('Paypal checkout does not match Phortune charge!'));
        }

        if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
          return $controller->newDialog()
            ->setTitle(pht('Payment Already Processed'))
            ->appendParagraph(
              pht(
                'The payment response for this charge attempt has already '.
                'been processed.'))
            ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
        }

        $price = $cart->getTotalPriceAsCurrency();

        $params = array(
          'TOKEN' => $token,
          'PAYERID' => $result['PAYERID'],

          'PAYMENTREQUEST_0_AMT'            => $price->formatBareValue(),
          'PAYMENTREQUEST_0_CURRENCYCODE'   => $price->getCurrency(),
          'PAYMENTREQUEST_0_PAYMENTACTION'  => 'Sale',
        );

        $result = $this
          ->newPaypalAPICall()
          ->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
          ->resolve();

        $transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];

        $success = false;
        $hold = false;
        switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
          case 'Processed':
          case 'Completed':
          case 'Completed-Funds-Held':
            $success = true;
            break;
          case 'In-Progress':
          case 'Pending':
            // TODO: We can capture more information about this stuff.
            $hold = true;
            break;
          case 'Denied':
          case 'Expired':
          case 'Failed':
          case 'Partially-Refunded':
          case 'Canceled-Reversal':
          case 'None':
          case 'Refunded':
          case 'Reversed':
          case 'Voided':
          default:
            // These are all failure states.
            break;
        }

        $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();

          $charge->setMetadataValue('paypal.transactionID', $transaction_id);
          $charge->save();

          if ($success) {
            $cart->didApplyCharge($charge);
            $response = id(new AphrontRedirectResponse())->setURI(
              $cart->getCheckoutURI());
          } else if ($hold) {
            $cart->didHoldCharge($charge);

            $response = $controller
              ->newDialog()
              ->setTitle(pht('Charge On Hold'))
              ->appendParagraph(
                pht('Your charge is on hold, for reasons?'))
              ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
          } else {
            $cart->didFailCharge($charge);

            $response = $controller
              ->newDialog()
              ->setTitle(pht('Charge Failed'))
              ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
          }
        unset($unguarded);

        return $response;
      case 'cancel':
        if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {
          $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
            // TODO: Since the user cancelled this, we could conceivably just
            // throw it away or make it more clear that it's a user cancel.
            $cart->didFailCharge($charge);
          unset($unguarded);
        }

        return id(new AphrontRedirectResponse())
          ->setURI($cart->getCheckoutURI());
    }

    throw new Exception(
      pht('Unsupported action "%s".', $controller->getAction()));
  }

  private function newPaypalAPICall() {
    if ($this->isAcceptingLivePayments()) {
      $host = 'https://api-3t.paypal.com/nvp';
    } else {
      $host = 'https://api-3t.sandbox.paypal.com/nvp';
    }

    return id(new PhutilPayPalAPIFuture())
      ->setHost($host)
      ->setAPIUsername($this->getPaypalAPIUsername())
      ->setAPIPassword($this->getPaypalAPIPassword())
      ->setAPISignature($this->getPaypalAPISignature());
  }


}
