<?php

/**
 * PrecisionPay_Payments_For_WC class
 *
 * @author   PrecisionPay <support@myprecisionpay.com>
 * @package  WooCommerce PrecisionPay Payments Gateway
 * @since    4.0.0
 */

// Exit if accessed directly.
if (!defined('ABSPATH')) {
  exit;
}


/**
 * PrecisionPay Gateway.
 * 
 * @class   PrecisionPay_Payments_For_WC
 * @version 4.0.4
 */
class PrecisionPay_Payments_For_WC extends WC_Payment_Gateway {
  public const TITLE               = 'PrecisionPay';
  public const NAME                = 'wc_gateway_precisionpay';
  public const DOMAIN              = 'precisionpay-payments-for-woocommerce';
  public const BRAND_COLOR         = '#F15A29';
  private const ECOMMERCE_PLATFORM = 'WooCommerce';

  // Predefined error messages
  const ERROR_MESSAGE_EXPIRED_PLAID_TOKEN   = 'Your account authorization has expired, authorizations expire after 30 minutes';
  const ERROR_MESSAGE_NO_BANK_ACCOUNT_FOUND = 'There are no valid checking or savings account(s) associated with this Item.';

  // Session constants
  private const SESSION_STORAGE_PRECISION_PAY = 'mcPrecisionPayData';
  private const SESSION_STORAGE_PLAID         = 'mcPlaidData';

  // Plaid Environments
  private const PLAID_ENV_SANDBOX    = 'sandbox';
  private const PLAID_ENV_PRODUCTION = 'production';

  // PrecisionPay Environments
  private const PP_ENV_SANDBOX    = 'sandbox';
  private const PP_ENV_PRODUCTION = 'live';

  // API URL
  private const API_URL_PROD = 'https://api.myprecisionpay.com/api';

  // Checkout portal URL
  private const CHECKOUT_PORTAL_URL_PROD = 'https://checkout.myprecisionpay.com';

  /**
   * Payment gateway instructions.
   * @var string
   * 
   */
  protected $instructions;

  /**
   * Unique id for the gateway.
   * @var string
   * 
   */
  public $id = self::NAME;

  /**
   * Logging.
   */
  private static $log_enabled = true;
  private static $log;

  /**
   * Additional public class variables.
   */
  public $plugin_url;
  public $logo;
  public $logo_white;
  public $version;
  public $button_title;
  public $wp_nonce_name;
  public $wp_nonce;
  public $enable_test_mode;
  public $is_ssl;
  public $has_api_keys;

  /**
   * Additional private class variables.
   */
  private $loading_icon;
  private $loading_icon_long;
  private $env;
  private $plaid_env;
  private $api_key;
  private $api_secret;
  private $api_key_header;
  private $api_url;
  private $checkout_url;

  /**
   * Constructor for the gateway.
   */
  public function __construct() {
    $this->plugin_url         = WC_PrecisionPay_Payments::plugin_url();
    $this->logo               = $this->get_option('logo') ? $this->get_option('logo') : $this->plugin_url . '/assets/img/precisionpay_logo_2x.png';
    $this->icon               = $this->logo; // Used to be just the mark, but it is needed for the legacy checkout label. OLD: $this->plugin_url . '/assets/img/logo-mark.svg';
    $this->logo_white         = $this->plugin_url . '/assets/img/precisionpay_logo_100percent_white_2x.png';
    $this->loading_icon       = $this->plugin_url . '/assets/img/pp_loading_screen_300.png';
    $this->loading_icon_long  = $this->plugin_url . '/assets/img/pp_loading_screen_w_text.png';
    $this->method_title       = _x('PrecisionPay', 'PrecisionPay payment method', self::DOMAIN);
    $this->method_description = __('Welcome to PrecisionPay, the Second Amendment payments company.<br />If you already have a merchant account, enter your keys below. If not, visit myprecisionpay.com to apply for a merchant account.', self::DOMAIN);
    $this->title              = self::TITLE;
    $this->description        = __('Use your bank account to protect your privacy and liberty with PrecisionPay: 2A payments made easy.', self::DOMAIN);
    $this->button_title       = __('Pay with', self::DOMAIN);
    $this->enabled            = $this->get_option('enabled'); // Option values: 'yes', 'no'
    $this->enable_test_mode   = 'yes' === $this->get_option('enable_test_mode');
    $this->has_fields         = true;
    $this->env                = $this->enable_test_mode ? self::PP_ENV_SANDBOX : self::PP_ENV_PRODUCTION;
    $this->plaid_env          = $this->enable_test_mode ? self::PLAID_ENV_SANDBOX : self::PLAID_ENV_PRODUCTION;
    $this->api_key            = $this->get_option('api_key');
    $this->api_secret         = $this->get_option('api_secret');
    $this->has_api_keys       = $this->api_key && $this->api_secret;
    $this->api_key_header     = json_encode(['apiKey' => $this->api_key, 'apiSecret' => $this->api_secret]);
    $this->api_url            = self::API_URL_PROD;
    $this->checkout_url       = self::CHECKOUT_PORTAL_URL_PROD;
    $this->wp_nonce_name      = 'precisionpay_nonce';
    $this->wp_nonce           = wp_create_nonce('precisionpay_gateway_nonce');
    $this->is_ssl             = is_ssl();
    $this->supports           = ['products', 'refunds'];

    // Load the settings
    $this->init_form_fields();
    $this->init_settings();

    // Define user set variables
    add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);

    // Admin actions
    add_action('admin_init', [$this, 'add_privacy_message']);
    add_action('admin_enqueue_scripts', [$this, 'admin_scripts']);

    // Custom CSS & custom JavaScript to obtain a token
    add_action('wp_enqueue_scripts', [$this, 'payment_scripts']);

    // Custom actions after checkout is complete
    add_action('woocommerce_thankyou_' . $this->id, [$this, 'thankyou_page']);
  }

  /**
   * Gets the privacy message for merchants
   */
  public function add_privacy_message() {
    if (!function_exists('wp_add_privacy_policy_content')) {
      return;
    }

    $content = wpautop(
      sprintf(
        wp_kses(
          __('By using this plugin, you consent to storage and use of personal data required to send and receive payments. <a href="%s" target="_blank">View the PrecisionPay Privacy Policy to see what you may want to include in your privacy policy.</a>', self::DOMAIN),
          array(
            'a' => array(
              'href'   => array(),
              'target' => array(),
            ),
          )
        ),
        'https://www.myprecisionpay.com/privacy-policy'
      )
    );

    wp_add_privacy_policy_content(PRCSNPY_PLUGIN_NAME, $content);
  }

  /**
   * Renders a custom settings page
   */
  public function admin_options() {
    $this->render_header();
    $this->render_settings();
  }

  /**
   * Renders the admin fields
   */
  public function render_settings() {
    echo '<table class="form-table">' . $this->generate_settings_html($this->get_form_fields(), false) . '</table>';
  }

  /**
   * Renders the header of the admin page
   * (found in WooCommerce->Settings->payments->PrecisionPay->Manage)
   */
  public function render_header() {
    echo '
    <div class="precisionpay-settings-page-header">
        <div class="top-section">
          <img alt="PrecisionPay" class="precisionpay-logo" width="380" src="' . esc_url($this->logo) . '"/>
          <h4 class="precisionpay-tagline">' . esc_html__('Fast, Secure, Payments, for the Second Amendment Community.', self::DOMAIN) . '</h4>
          <a class="button" target="_blank" href= "https://developers.myprecisionpay.com/">'
      . __('Documentation', self::DOMAIN) .
      '</a>
          <a class="button" target="_blank" href="mailto:support@myprecisionpay.com">'
      . esc_html__('Get Help', self::DOMAIN) .
      '</a>
      </span>
        </div>
        <h3>Welcome to PrecisionPay, the Second Amendment payments company.</h3>
        <p>If you already have a merchant account, enter your keys below. If not, visit 
        <a target="_blank" href="https://www.myprecisionpay.com/merchant-sign-up#merchant-application">myprecisionpay.com</a> 
        to apply for a merchant account.</p>
      </div>
      ';

    // TODO: add these links once we have the corresponding pages
    //     <span class="precisionpay-right-align">
    //       <a target="_blank" href="https://myprecisionpay.com/feature-requests/woocommerce-precisionpay/">'
    // . __('Request a feature', self::DOMAIN) .
    // '</a>
    //       <a target="_blank" href="https://github.com/MakeCents-NYC/woocommerce-gateway-precisionpay/issues/new?assignees=&labels=type%3A+bug&template=bug_report.md">'
    // . __('Submit a bug', self::DOMAIN) .
    // '</a>
  }

  /**
   * Custom JS for admin side of the plugin
   */
  public function admin_scripts($hook) {
    // Only add to the PrecisionPay woocommerce settings page
    if ('woocommerce_page_wc-settings' !== $hook) {
      return;
    }

    wp_enqueue_style('precisionpay-admin-styles', $this->plugin_url . '/assets/css/admin.css');

    // Only add this script if SSL is not enabled
    if (!$this->is_ssl) {
      wp_enqueue_script('mc_admin_script_ssl', $this->plugin_url . '/assets/js/admin-script-ssl.js');
    }
  }

  /**
   * Get the merchant API Key for use with PrecisionPay Login
   */
  public function get_merchant_nonce() {
    // Make sure the API Key has been added first
    $merchantApiKey = $this->api_key;

    if (!$merchantApiKey) {
      $errorNotice = __('We are unable to process your pay request at the moment. Please refresh the page and try again', self::DOMAIN);
      $this->ajaxFailedResponse($errorNotice);
      return;
    }

    $response = $this->api_get('/merchant-nonce');

    if (is_wp_error($response)) {
      $errorNotice = __('Error processing server request.', self::DOMAIN);
      $this->ajaxFailedResponse($errorNotice);
      return;
    }

    $response_body = wp_remote_retrieve_body($response);


    $response_data = json_decode($response_body);

    if (empty($response_body)) {
      $errorNotice = __('Error processing server request at this time. Please try again later.', self::DOMAIN);
      $this->ajaxFailedResponse($errorNotice);
      return;
    }

    if (!$response_data->merchantNonce) {
      // $errorNotice = $response_body;
      $errorNotice = __('Error processing server request at this time. Please try again later.', self::DOMAIN);
      $this->ajaxFailedResponse($errorNotice);
      return;
    }

    $merchantNonce = $response_data->merchantNonce;

    wp_send_json(
      array(
        'result'  => 'success',
        'message' => 'nonce retrieved',
        'body'    => array(
          'merchantNonce'  => $merchantNonce
        ),
      )
    );
  }

  /**
   * Initialize Gateway Settings Form Fields.
   */
  public function init_form_fields() {
    $this->form_fields = [
      'enabled' => [
        'title'   => __('Enable/Disable', self::DOMAIN),
        'type'    => 'checkbox',
        'label'   => __('Enable PrecisionPay Payment Gateway', self::DOMAIN),
        'default' => 'yes',
      ],
      'enable_test_mode' => [
        'title'       => __('Enable Test Mode', self::DOMAIN),
        'label'       => __('Enable Test Mode', self::DOMAIN),
        'type'        => 'checkbox',
        'description' => __('Place the payment gateway in test mode to test the plugin without needing to spend any money.', self::DOMAIN),
        'default'     => '',
        'desc_tip'    => true,
      ],
      'api_secret' => [
        'title' => __('API Secret', self::DOMAIN),
        'type'  => 'password'
      ],
      'api_key' => [
        'title' => __('API Key', self::DOMAIN),
        'type'  => 'text'
      ],
      'logo' => [
        'title'   => __('Logo Displayed on Checkout Page', self::DOMAIN),
        'type'    => 'select',
        'options' =>
        [
          $this->plugin_url . '/assets/img/precisionpay_logo_2x.png' => __('default (standard logo)', self::DOMAIN),
          $this->plugin_url . '/assets/img/precisionpay_logo_white_2x.png' => __('darkmode (light logo for dark backgrounds)', self::DOMAIN),
          $this->plugin_url . '/assets/img/precisionpay_logo_white_bg_2x.png' => __('slug (standard with white box background)', self::DOMAIN),
        ],
      ],
    ];
  }

  /**
   * Custom CSS and JS
   */
  public function payment_scripts() {
    // We only need to add styles and scripts on cart/checkout and pay-for-order pages
    if (!is_cart() && !is_checkout() && !isset($_GET['pay_for_order'])) {
      return;
    }

    // If our payment gateway is disabled, we do not have to enqueue JS
    if ('no' === $this->enabled) {
      return;
    }

    // Add styles
    wp_enqueue_style('precisionpay-styles', $this->plugin_url . '/assets/css/main.css', array(), PRCSNPY_VERSION);

    // Require SSL unless the website is in a test mode
    if (!$this->enable_test_mode && !$this->is_ssl) {
      return;
    }

    // Using Plaid Link to obtain a token
    wp_enqueue_script('plaid_link', 'https://cdn.plaid.com/link/v2/stable/link-initialize.js', array(), null, true);

    // Scripts to handle authorize payment button
    // Let's check to see if this is an invoice (if it is then the page will have an order-pay parameter). 
    $order_amount = WC()->cart->total; // In the checkout page we'll use the cart total
    if (isset($_GET['order-pay'])) {
      $order_id = sanitize_text_field($_GET['order-pay']);
      $order = wc_get_order($order_id);
      $order_amount = $order->get_total();
    }

    // For legacy checkout
    wp_enqueue_script(
      'pp-loader-script',
      $this->plugin_url . '/assets/js/pp-loader.js',
      array('jquery'),
      PRCSNPY_VERSION,
      array('in_footer' => true,)
    );

    // For both legacy and block checkout
    wp_enqueue_script(
      'precisionpay-script',
      $this->plugin_url . '/assets/js/precisionpay.js',
      array('jquery'),
      PRCSNPY_VERSION,
      array('in_footer' => true,)
    );

    wp_localize_script(
      'precisionpay-script',
      'precisionpay_data',
      [
        'precisionPayNonce' => wp_create_nonce("precision_pay_ajax_nonce"),
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'orderAmount' => $order_amount,
        'errorMessageTokenExpired' => __('Your PrecisionPay token expired, please log back in again', self::DOMAIN),
        'errorMessagePlaidTokenExpired' => __("We're sorry, your PrecisionPay session has expired. Please reauthorize payment to complete your purchase.", self::DOMAIN),
        'errorMessageNoValidAccounts' => __('There are no valid checking or savings accounts associated with this bank. Please reauthorize payment to fix this error.', self::DOMAIN),
        'errorMessageInsufficientFunds' => __('Insufficient funds in authorized bank account. Please use another account to pay for this transaction.', self::DOMAIN),
        'defaultButtonTitle' => $this->button_title,
        'buttonLogo' => $this->logo_white,
        'loadingImg' => $this->loading_icon,
        'loadingImgLong' => $this->loading_icon_long,
        'plaidEnv' => $this->plaid_env,
        'checkoutPortalURL' => $this->checkout_url,
      ]
    );
  }

  /**
   * Needed for legacy (non-blocks) payment gateway form
   */
  public function payment_fields() {
    // Let's require SSL unless the website is in test mode
    if (!$this->enable_test_mode && !$this->is_ssl) {
      echo '<div>
              <p class="error" style="color: red">
      ' . esc_html__(
        'SSL is required for the PrecisionPay payment gateway. Please enable SSL on your site to continue.',
        self::DOMAIN
      ) . '
            </p>
          </div>';
      return;
    }

    // We want the business owner to know that the api key and secret are necessary so they don't go live without them
    if (!$this->has_api_keys) {
      echo '<p class="error" style="color: red">The ' . esc_html($this->title) . ' plugin is not fully configured yet and will not work. Please complete the configuration process before going live with this plugin.</p>';
      return;
    }

    if ($this->enable_test_mode) {
      $this->description .= ' TEST MODE ENABLED. Use this mode for testing purposes only. You can find the Guest/One-time-pay test credentials in this <a href="https://plaid.com/docs/quickstart/" target="_blank" rel="noopener noreferrer">documentation</a>.';
      $this->description  = trim($this->description);
    }

    if ($this->description != '') {
      // display the description with <p> tags
      echo wpautop(wp_kses_post($this->description));
    }

    $nonce_field = '<input type="hidden" id="' . $this->wp_nonce_name . '" name="' . $this->wp_nonce_name . '" value="' . $this->wp_nonce . '" />';
?>
    <fieldset id="wc-<?php echo esc_attr($this->id); ?>-mc-form" class="wc-precisionpay-form wc-payment-form" style="background:transparent;">
      <div style="display: none;">
        <div class="bank-plaid">
          <input name="precisionpay_public_token" id="precisionpay_public_token" type="hidden" />
          <input name="precisionpay_account_id" id="precisionpay_account_id" type="hidden" />
          <input name="precisionpay_plaid_user_id" id="precisionpay_plaid_user_id" value="" type="hidden" />
          <input name="precisionpay_registered_user_id" id="precisionpay_registered_user_id" value="" type="hidden" />
        </div>
        <div class="bank-precision-pay-member">
          <input name="precisionpay_checkout_token" id="precisionpay_checkout_token" value="" type="hidden" />
        </div>
        <?php echo $nonce_field ?>
      </div>
      <button id="precisionpay-link-button" class="precisionpay-plaid-link-button precisionpay-button" style="background-color: <?php echo esc_html(self::BRAND_COLOR); ?>;">
        <span><?php echo esc_html($this->button_title); ?></span> <img src="<?php echo esc_url($this->logo_white); ?>" alt="PrecisionPay" />
      </button>
      <div class="clear"></div>
      <div class="clear"><img class="precisionPayLoadingFullPNG" src="<?php echo esc_url($this->loading_icon_long); ?>" /></div>
      <script type="text/javascript">
        jQuery(function() {
          var precisionPayPaymentGateway = usePrecisionPayPaymentGateway(jQuery);
          precisionPayPaymentGateway.init();
        });
      </script>
    </fieldset>
<?php
  }

  /**
   * Fields validation before process_payment() is triggered
   * 
   * @return boolean
   */
  public function validate_fields() {
    if (!isset($_POST['precisionpay_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['precisionpay_nonce'])), "precisionpay_gateway_nonce")) {
      $errorNotice = __('Server Error validating nonce. Please try again.');
      wc_add_notice($errorNotice, 'error');
      return false;
    }

    if (empty($_POST['precisionpay_public_token']) && empty($_POST['precisionpay_checkout_token']) && empty($_POST['precisionpay_creditcard_token'])) {
      wc_add_notice('You must authorize the payment before you can complete the order', 'error');
      return false;
    }
    return true;
  }

  /**
   * Once the form is validated we can process the payment.
   * First we'll process the payment with the PrecisionPay API.
   * Next we'll go through the woocommerce checkout process to finish up.
   * 
   * @param int $order_id
   * @return array
   */
  public function process_payment($order_id) {
    global $woocommerce;
    $order = wc_get_order($order_id);
    $curr_cart = WC()->cart->get_cart();
    $payResponse_body = null;
    $precisionpayCheckoutToken = isset($_POST['precisionpay_checkout_token'])
      ? sanitize_text_field($_POST['precisionpay_checkout_token'])
      : null;
    $precisionpayPlaidToken = isset($_POST['precisionpay_public_token'])
      ? sanitize_text_field($_POST['precisionpay_public_token'])
      : null;

    if ($precisionpayCheckoutToken) {
      $payResponse_body = $this->pay_with_precisionpay($order, $order_id, $precisionpayCheckoutToken, $curr_cart);
    } else if ($precisionpayPlaidToken) {
      $payResponse_body = $this->pay_with_plaid($order, $order_id, $curr_cart);
    }

    try {
      if ($payResponse_body && (isset($payResponse_body->message) && $payResponse_body->message == 'success')) {
        $order->add_order_note(__('Customer successfully paid through PrecisionPay payment gateway', self::DOMAIN));
        wc_reduce_stock_levels($order_id);
        $order->payment_complete();
        $woocommerce->cart->empty_cart();

        // Redirect to thank you page
        return [
          'result'   => 'success',
          'redirect' => $this->get_return_url($order),
        ];
      } else {
        // Transaction was not successful ...Add notice to the cart
        $errorNotice = __('An unknown error occured while attempting to charge your account', self::DOMAIN);
        $responseErrorMessage = $payResponse_body ? $payResponse_body->detail : null;
        if ($responseErrorMessage) {
          switch ($responseErrorMessage) {
            case 'PrecisionPay token invalid':
              // Handle expired token
              $errorNotice = __('Your PrecisionPay token expired, please log back in again', self::DOMAIN);
              break;
            case self::ERROR_MESSAGE_EXPIRED_PLAID_TOKEN:
              // $errorNotice = __('Your account authorization has expired, authorizations expire after 30 minutes', self::DOMAIN);
              $errorNotice = __("We're sorry, your PrecisionPay session has expired. Please reauthorize payment to complete your purchase.", self::DOMAIN);
              break;
            case self::ERROR_MESSAGE_NO_BANK_ACCOUNT_FOUND:
              $errorNotice = __('There are no valid checking or savings accounts associated with this bank. Please reauthorize payment to fix this error.', self::DOMAIN);
              break;
            case 'We are unable to proccess your payment due insufficient funds in your account':
            case 'We are unable to proccess your payment due to insufficient funds in your account':
            case "You don't have enough funds in your account for this transaction":
              $errorNotice = __('Insufficient funds in authorized bank account. Please use another account to pay for this transaction.', self::DOMAIN);
              break;
            default:
              $errorNotice = '';
              if (is_array($responseErrorMessage)) {
                $errorNotice .= $payResponse_body->detail[0];
              } else {
                $errorNotice .= $payResponse_body->detail;
              }
              break;
          }
        }

        wc_add_notice($errorNotice, 'error');

        // Also add note to the order for wp-admin reference
        $order->add_order_note('Error: ' . $errorNotice);

        return [
          'result'   => 'failure',
        ];
      }
    } catch (Exception $e) {
      wc_add_notice($e->getMessage(), 'error');

      return [
        'result'   => 'failure',
      ];
    }
  }

  /**
   * Checks that the hidden fields have expected values
   * 
   * @param string $field
   * @param string $message
   */
  private function validate_hidden_field($field, $message = '') {
    if (!preg_match("/^[0-9a-zA-Z\-]+$/", $field)) {
      $error_message = __('Error processing payment.', self::DOMAIN) . ' ' . $message;
      throw new Exception($error_message);
    }
  }


  /**
   * Returns a subset of cart item properties.
   * 
   * @param array
   * @return array
   */
  private function simplify_cart_item($item) {
    if (!isset($item)) {
      return [];
    }

    $product = $item['data']; // Object: WC_Product_Simple
    $currated_item = [];

    if (isset($product)) {
      $currated_item = [
        'name' => $product->get_name(),
        'short_description' => $product->get_short_description(),
        'price' => $product->get_price(),
        'quantity' => $item['quantity'],
        'line_total' => $item['line_total'],
      ];
    }

    return $currated_item;
  }

  /**
   * Pay for order with a logged in PrecisionPay user's account on file
   * 
   * @param object $order
   * @param int $order_id
   * @param string $precisionpayCheckoutToken
   * @return object
   */
  private function pay_with_precisionpay($order, $order_id, $precisionpayCheckoutToken, $cart) {
    // Validate field first
    $this->validate_hidden_field($precisionpayCheckoutToken, __('Invalid checkout token.', self::DOMAIN));

    $currated_cart = array_map([$this, 'simplify_cart_item'], $cart);
    $orderNumber = $this->get_order_number($order, $order_id);
    $paymentData = array(
      'precisionPayToken' => $precisionpayCheckoutToken,
      'amount'            => floatval($order->get_total()),
      'order'             => strval($orderNumber),
      'cart'              => json_encode($currated_cart),
      'platform'          => self::ECOMMERCE_PLATFORM,
      'env'               => $this->env,
    );

    $payResponse = $this->api_post('/checkout/pay', $paymentData);

    if (is_wp_error($payResponse)) {
      throw new Exception(__('Error processing payment.', self::DOMAIN));
    }

    if (empty($payResponse['body'])) {
      throw new Exception(__('Error processing payment at this time. Please try again later.', self::DOMAIN));
    }

    // Retrieve the body's response if no top level errors found
    $payResponse_body = json_decode(wp_remote_retrieve_body($payResponse));

    return $payResponse_body;
  }

  /**
   * Pay with a bank account linked through Plaid (guest checkout)
   * 
   * @param object $order
   * @param int $order_id
   * @return object
   */
  private function pay_with_plaid($order, $order_id, $cart) {
    $publicToken = sanitize_text_field($_POST['precisionpay_public_token']);
    $accountId = sanitize_text_field($_POST['precisionpay_account_id']);
    $precisionpay_user_id = sanitize_text_field($_POST['precisionpay_plaid_user_id']);
    $precisionpay_registered_user_id = isset($_POST['precisionpay_registered_user_id'])
      ? sanitize_text_field($_POST['precisionpay_registered_user_id'])
      : null;

    // Make sure the plaid link returned what we needed
    if (!$publicToken || !$accountId || !$precisionpay_user_id) {
      throw new Exception(__('Your bank account is not linked. Please link your account.', self::DOMAIN));
    }

    // validate the input:
    $this->validate_hidden_field($publicToken, __('Invalid token.', self::DOMAIN));
    $this->validate_hidden_field($accountId, __('Invalid id.', self::DOMAIN));
    $this->validate_hidden_field($precisionpay_user_id, __('Invalid Plaid user id.', self::DOMAIN));

    // This is optional and only gets set if the customer logs into our Customer Portal but does one-time-pay with Plaid
    if ($precisionpay_registered_user_id) {
      $this->validate_hidden_field($precisionpay_registered_user_id, __('Invalid PrecisionPay Account.', self::DOMAIN));
    }

    $orderNumber = $this->get_order_number($order, $order_id);
    $currated_cart = array_map([$this, 'simplify_cart_item'], $cart);

    $paymentData = array(
      'public_token'     => $publicToken,
      'account_id'       => $accountId,
      'first_name'       => $order->get_billing_first_name(),
      'last_name'        => $order->get_billing_last_name(),
      'business_name'    => $order->get_billing_company(), //? $order->get_billing_company() : 'PrecisionPay Default',
      'email'            => $order->get_billing_email(),
      'one_time_user_id' => $precisionpay_user_id, // string
      'external_user_id' => $precisionpay_registered_user_id, // We need to pass this, but it will usually be an empty string
      'amount'           => floatval($order->get_total()), // number (int or decimal)
      'order'            => strval($orderNumber), // <- Needs to come in as a string
      'cart'             => json_encode($currated_cart),
      'platform'         => self::ECOMMERCE_PLATFORM,
      'env'              => $this->env, // Lets API know if we are in sandbox or live mode
    );

    $payResponse = $this->api_post('/checkout/one-time-payment', $paymentData);

    if (is_wp_error($payResponse)) {
      throw new Exception(__('Error processing payment.', self::DOMAIN));
    }

    if (empty($payResponse['body'])) {
      throw new Exception(__('Error processing payment at this time. Please try again later.', self::DOMAIN));
    }

    // Retrieve the body's response if no top level errors found
    $payResponse_body = json_decode(wp_remote_retrieve_body($payResponse));

    return $payResponse_body;
  }

  /**
   * Output for the order received page.
   */
  public function thankyou_page() {

    wp_enqueue_script(
      'precisionpay-thankyou-script',
      $this->plugin_url . '/assets/js/thankyou.js',
      array('jquery'),
      PRCSNPY_VERSION,
      array('strategy' => 'defer', 'in_footer' => true,)
    );
    wp_localize_script(
      'precisionpay-thankyou-script',
      'precisionpay_ty_data',
      array(
        'sessionStoragePrecisionPay' => esc_js(self::SESSION_STORAGE_PRECISION_PAY),
        'sessionStoragePlaid' => esc_js(self::SESSION_STORAGE_PLAID),
      )
    );
  }

  /**
   * Process a refund and send info to PrecisionPay API.
   * 
   * @param string|int $order_id
   * @param string|null $amount
   * @param string|null $reason
   * @return boolean|object
   */
  public function process_refund($order_id, $amount = null, $reason = '') {
    // Step 1: Get the order
    $order = wc_get_order($order_id);

    if (! $this->can_refund_order($order)) {
      return new WP_Error('error', __('Refund failed.', 'woocommerce'));
    }

    // if no amount set to full amount
    if ($amount == null) {
      $amount = $order->get_total();
    }

    if (floatval($amount) <= 0) {
      return new WP_Error('error', 'Please specify an amount greater than 0.');
    }

    // Step 2: Check that amount <= the order amount (This appears to be handled by WooCommerce)
    if (floatval($order->get_total()) < floatval($amount)) {
      return new WP_Error('Refund amount is greater than order amount', 'error');
    }

    // Step 3: Make a call to refund payment - PrecisionPay API
    $orderNumber = $this->get_order_number($order, $order_id);
    $refundData = array(
      'order'  => strval($orderNumber),
      'reason' => strval($reason),
      'note'   => '',
      'amount' => floatval($amount),
    );
    $refundResponse = $this->api_post('/checkout/refund', $refundData);

    if (is_wp_error($refundResponse)) {
      static::log('Refund Failed: ' . $refundResponse->get_error_message(), 'error');
      return new WP_Error('error', $refundResponse->get_error_message());
    } else if (empty($refundResponse['body'])) {
      return new WP_Error('error', __('Error processing refund at this time. Please try again later.', self::DOMAIN));
    }

    $refundResponse_body = json_decode(wp_remote_retrieve_body($refundResponse));

    if (!$refundResponse_body || !isset($refundResponse_body->message) || !$refundResponse_body->message == 'success') {
      $responseErrorMessage = $refundResponse_body ? $refundResponse_body->detail : '';
      return new WP_Error('error', $responseErrorMessage);
    }

    // Step 4: If success return true
    return true;
  }

  /**
   * Return a failed response to the front end.
   */
  private function ajaxFailedResponse($message) {
    wp_send_json(
      array(
        'result'  => 'failed',
        'message' => $message,
      )
    );
  }

  /**
   * Returns the order number if it exists otherwise just the order ID
   * 
   * @param object $order
   * @param int $order_id
   * @return int|string
   */
  private function get_order_number($order, $order_id) {
    $orderNumber = $order_id;
    $metaOrderNumber = $order->get_meta('_order_number');

    // Check for custom order number
    if ($metaOrderNumber) {
      $orderNumber = $metaOrderNumber;
    } else {
      $possibleOrderNumber = $order->get_order_number();

      if ($possibleOrderNumber) {
        $orderNumber = $possibleOrderNumber;
      }
    }

    return $orderNumber;
  }

  /**
   * Make a post request to the PrecisionPay API.
   * 
   * @param string $endpoint
   * @param array $paymentData
   * @return object
   */
  private function api_post($endpoint, $paymentData) {
    $referer = sanitize_url($_SERVER['HTTP_REFERER']);
    $response = wp_remote_post($this->api_url . $endpoint, array(
      'method'  => 'POST',
      'headers' => array(
        'Accept'        => 'application/json',
        'Content-Type'  => 'application/json',
        'X-Application-Access' => $this->api_key_header,
        'Referer' => $referer
      ),
      'body'    => json_encode($paymentData), // http_build_query($payload),
      'timeout' => 90,
    ));

    return $response;
  }

  /**
   * Make a get request to the PrecisionPay API.
   * 
   * @param string $endpoint
   * @return object
   */
  private function api_get($endpoint) {
    $referer = sanitize_url($_SERVER['HTTP_REFERER']);
    $response = wp_remote_get($this->api_url . $endpoint, array(
      'headers' => array(
        'Accept'        => 'application/json',
        'Content-Type'  => 'application/json',
        'X-Application-Access' => $this->api_key_header,
        'Referer' => $referer
      ),
      'timeout' => 90,
    ));

    return $response;
  }

  /**
   * Logging method.
   *
   * @param string $message Log message.
   * @param string $level Optional. Default 'info'. Possible values:
   *                   emergency|alert|critical|error|warning|notice|info|debug.
   */
  public static function log($message, $level = 'info') {
    if (self::$log_enabled) {
      if (empty(self::$log)) {
        self::$log = wc_get_logger();
      }
      self::$log->log($level, $message, array('source' => 'precisionpay'));
    }
  }
}
