<?php
/**
 * PayPal Commerce Functions
 *
 * @package    easy-digital-downloads
 * @subpackage Gateways\PayPal
 * @copyright  Copyright (c) 2021, Sandhills Development, LLC
 * @license    GPL2+
 * @since      2.11
 */

namespace EDD\Gateways\PayPal;

use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
use EDD\Gateways\PayPal\V3\Onboarding;

/**
 * Determines whether or not there's a valid REST API connection.
 *
 * @param string $mode Mode to check (`live` or `sandbox`).
 *
 * @since 2.11
 * @return bool
 */
function has_rest_api_connection( $mode = '' ) {
	try {
		$api = new API( $mode );

		return true;
	} catch ( Authentication_Exception $e ) {
		return false;
	}
}

/**
 * Determines whether or not the account is ready to accept payments.
 * Requirements:
 *
 *      - API keys must be set.
 *      - Merchant account must be ready to accept payments.
 *
 * @see API::set_credentials()
 * @see AccountStatusValidator::check_merchant_account()
 *
 * @since 2.11
 *
 * @param string $mode
 *
 * @return bool
 */
function ready_to_accept_payments( $mode = '' ) {
	if ( empty( $mode ) ) {
		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
	}

	// v3 (Connect): onboarding is complete when store_id, hmac_key, and merchant_id are all present.
	if ( 'v3' === get_option( "edd_paypal_{$mode}_commerce_version", 'v3' ) ) {
		return Onboarding::is_v3_onboarded( $mode );
	}

	if ( ! has_rest_api_connection( $mode ) ) {
		return false;
	}

	$validator = new AccountStatusValidator( $mode );
	$validator->check_merchant_account();

	return empty( $validator->errors_for_merchant_account->errors );
}

/**
 * Determines whether or not PayPal Standard should be enabled.
 * This returns true if the store owner previously had a PayPal Standard connection but has not yet
 * connected to the new REST API implementation.
 *
 * If PayPal Standard is enabled, then PayPal payments run through the legacy API.
 *
 * @param string $mode If omitted, current site mode is used.
 *
 * @since 2.11
 * @return bool
 */
function paypal_standard_enabled( $mode = '' ) {
	if ( empty( $mode ) ) {
		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
	}

	$rest_connection = has_rest_api_connection( $mode );
	$enabled         = ! $rest_connection && edd_get_option( 'paypal_email' );

	/**
	 * Filters whether or not PayPal Standard is enabled.
	 *
	 * @since 2.11
	 *
	 * @param bool $enabled
	 */
	return apply_filters( 'edd_paypal_standard_enabled', $enabled );
}

/**
 * Returns the partner merchant ID for a given mode.
 *
 * @param string $mode If omitted, current site mode is used.
 *
 * @since 2.11
 * @return string
 */
function get_partner_merchant_id( $mode = '' ) {
	if ( empty( $mode ) ) {
		$mode = edd_is_test_mode() ? API::MODE_SANDBOX : API::MODE_LIVE;
	}

	if ( API::MODE_LIVE === $mode ) {
		return EDD_PAYPAL_MERCHANT_ID;
	} else {
		return EDD_PAYPAL_SANDBOX_MERCHANT_ID;
	}
}

/**
 * Returns the styles used for the PayPal buttons.
 *
 * @return array
 */
function get_button_styles() {
	$styles = array(
		'layout' => 'vertical',
		'size'   => 'responsive',
		'shape'  => 'rect',
		'color'  => 'gold',
		'label'  => 'paypal'
	);

	if ( ! edd_is_checkout() ) {
		$styles['layout'] = 'horizontal';
		$styles['label']  = 'buynow';
	}

	/**
	 * Filters the button styles.
	 *
	 * @since 2.11
	 */
	return apply_filters( 'edd_paypal_smart_button_style', $styles );
}

/**
 * Gets the PayPal purchase units without the individual item breakdown.
 *
 * @since 2.11.2
 *
 * @param int   $payment_id    The payment/order ID.
 * @param array $purchase_data The array of purchase data.
 * @param array $payment_args  The array created to insert the payment into the database.
 *
 * @return array
 */
function get_order_purchase_units_without_breakdown( $payment_id, $purchase_data, $payment_args ) {
	$order_amount = array(
		'currency_code' => edd_get_currency(),
		'value'         => (string) edd_sanitize_amount( $purchase_data['price'] ),
	);
	if ( (float) $purchase_data['tax'] > 0 ) {
		$order_amount['breakdown'] = array(
			'item_total' => array(
				'currency_code' => edd_get_currency(),
				'value'         => (string) edd_sanitize_amount( $purchase_data['price'] - $purchase_data['tax'] )
			),
			'tax_total'  => array(
				'currency_code' => edd_get_currency(),
				'value'         => (string) edd_sanitize_amount( $purchase_data['tax'] ),
			)
		);
	}

	return array(
		'reference_id' => $payment_args['purchase_key'],
		'amount'       => $order_amount,
		'custom_id'    => $payment_id
	);
}

/**
 * Gets the PayPal purchase units. The order breakdown includes the order items, tax, and discount.
 *
 * @since 2.11.2
 * @param int   $payment_id    The payment/order ID.
 * @param array $purchase_data The array of purchase data.
 * @param array $payment_args  The array created to insert the payment into the database.
 * @return array
 */
function get_order_purchase_units( $payment_id, $purchase_data, $payment_args ) {

	$currency       = edd_get_currency();
	$order_subtotal = $purchase_data['subtotal'];
	$items          = get_order_items( $purchase_data );
	// Adjust the order subtotal if any items are discounted.
	foreach ( $items as &$item ) {
		// A discount can be negative, so cast it to an absolute value for comparison.
		if ( (float) abs( $item['discount'] ) > 0 ) {
			$order_subtotal -= $item['discount'];
		}

		// The discount amount is not passed to PayPal as part of the $item.
		unset( $item['discount'] );
	}

	$discount = 0;
	// Fees which are not item specific need to be added to the PayPal data as order items.
	if ( ! empty( $purchase_data['fees'] ) ) {
		foreach ( $purchase_data['fees'] as $fee ) {
			if ( ! empty( $fee['download_id'] ) ) {
				continue;
			}
			// Positive fees.
			if ( floatval( $fee['amount'] ) > 0 ) {
				$items[]         = array(
					'name'        => stripslashes_deep( html_entity_decode( wp_strip_all_tags( $fee['label'] ), ENT_COMPAT, 'UTF-8' ) ),
					'unit_amount' => array(
						'currency_code' => $currency,
						'value'         => (string) edd_sanitize_amount( $fee['amount'] ),
					),
					'quantity'    => 1,
				);
				$order_subtotal += abs( $fee['amount'] );
			} else {
				// This is a negative fee (discount) not assigned to a specific Download
				$discount += abs( $fee['amount'] );
			}
		}
	}

	$order_amount = array(
		'currency_code' => $currency,
		'value'         => (string) edd_sanitize_amount( $purchase_data['price'] ),
		'breakdown'     => array(
			'item_total' => array(
				'currency_code' => $currency,
				'value'         => (string) edd_sanitize_amount( $order_subtotal ),
			),
		),
	);

	$tax = (float) $purchase_data['tax'] > 0 ? $purchase_data['tax'] : 0;
	if ( $tax > 0 ) {
		$order_amount['breakdown']['tax_total'] = array(
			'currency_code' => $currency,
			'value'         => (string) edd_sanitize_amount( $tax ),
		);
	}

	// This is only added by negative global fees.
	if ( $discount > 0 ) {
		$order_amount['breakdown']['discount'] = array(
			'currency_code' => $currency,
			'value'         => (string) edd_sanitize_amount( $discount ),
		);
	}

	return array(
		wp_parse_args( array(
			'amount' => $order_amount,
			'items'  => $items
		), get_order_purchase_units_without_breakdown( $payment_id, $purchase_data, $payment_args ) )
	);
}

/**
 * Gets an array of order items, formatted for PayPal, from the $purchase_data.
 *
 * @since 2.11.2
 * @param array $purchase_data
 * @return array
 */
function get_order_items( $purchase_data ) {
	// Create an array of items for the order.
	$items = array();
	if ( ! is_array( $purchase_data['cart_details'] ) || empty( $purchase_data['cart_details'] ) ) {
		return $items;
	}
	$i = 0;
	foreach ( $purchase_data['cart_details'] as $item ) {
		$item_amount = ( $item['subtotal'] / $item['quantity'] ) - ( $item['discount'] / $item['quantity'] );

		if ( $item_amount <= 0 ) {
			$item_amount = 0;
		}

		$substr_func = function_exists( 'mb_substr' ) ? 'mb_substr' : 'substr';
		$name        = $substr_func( edd_get_cart_item_name( $item ), 0, 127 );

		$items[ $i ] = array(
			'name'        => stripslashes_deep( html_entity_decode( $name, ENT_COMPAT, 'UTF-8' ) ),
			'quantity'    => $item['quantity'],
			'unit_amount' => array(
				'currency_code' => edd_get_currency(),
				'value'         => (string) edd_sanitize_amount( $item_amount ),
			),
			'discount'    => $item['discount'], // This is unset later and never sent to PayPal.
		);
		if ( edd_use_skus() ) {
			$sku = edd_get_download_sku( $item['id'] );
			if ( ! empty( $sku ) && '-' !== $sku ) {
				$items[ $i ]['sku'] = $sku;
			}
		}
		$i++;
	}

	return $items;
}

/**
 * Attempts to detect if there's an item total mismatch. This means the individual item breakdowns don't
 * add up to our proposed totals.
 *
 * @link https://github.com/easydigitaldownloads/easy-digital-downloads/pull/8835#issuecomment-921759101
 * @internal Not intended for public use.
 *
 * @since 2.11.2
 *
 * @param object $response
 *
 * @return bool
 */
function _is_item_total_mismatch( $response ) {
	if ( ! isset( $response->details ) || ! is_array( $response->details ) ) {
		return false;
	}

	foreach( $response->details as $detail ) {
		if ( ! empty( $detail->issue ) && 'ITEM_TOTAL_MISMATCH' === strtoupper( $detail->issue ) ) {
			return true;
		}
	}

	return false;
}

/**
 * Determines whether a host is acceptable to PayPal as a Fastlane domain.
 *
 * PayPal's `/v3/paypal/sdk-token` rejects the whole request with
 * `invalid_domain` if any single entry in the `domains` array is not a
 * publicly-resolvable hostname. Local development hosts like `multisite.local`
 * or `site.test`, raw IP addresses, and bare hostnames all fail validation.
 *
 * @since 3.6.9
 *
 * @param string $domain The host (no protocol, no path).
 * @return bool True if the domain is in a format PayPal will accept.
 */
function is_public_paypal_domain( $domain ) {
	if ( ! is_string( $domain ) ) {
		return false;
	}

	$domain = strtolower( trim( $domain ) );

	if ( '' === $domain || 'localhost' === $domain ) {
		return false;
	}

	// Reject raw IP addresses (v4 and v6).
	if ( false !== filter_var( $domain, FILTER_VALIDATE_IP ) ) {
		return false;
	}

	// Must contain at least one dot (single-label hosts are not public).
	if ( false === strpos( $domain, '.' ) ) {
		return false;
	}

	// Reject reserved / non-public TLDs commonly used for local development.
	$private_tlds = array( 'local', 'test', 'localhost', 'invalid', 'example', 'internal', 'lan', 'home', 'corp' );
	$parts        = explode( '.', $domain );
	$tld          = end( $parts );

	return ! in_array( $tld, $private_tlds, true );
}
