<?php
/**
 * Abstract PeachPay Authorize.net WC gateway.
 *
 * @PHPCS:disable Squiz.Commenting.VariableComment.Missing
 *
 * @package PeachPay
 */

defined( 'PEACHPAY_ABSPATH' ) || exit;

require_once PEACHPAY_ABSPATH . 'core/abstract/class-peachpay-payment-gateway.php';
require_once PEACHPAY_ABSPATH . 'core/payments/authnet/traits/trait-authnet-gateway-utilities.php';
require_once PEACHPAY_ABSPATH . 'core/payments/authnet/traits/trait-authnet-gateway-settings.php';

/**
 * The base Authorize.net gateway class.
 */
abstract class PeachPay_Authnet_Payment_Gateway extends PeachPay_Payment_Gateway {

	use PeachPay_Authnet_Gateway_Utilities;
	use PeachPay_Authnet_Gateway_Settings;

	public $payment_provider = 'authnet';
	public $min_max_currency = 'USD';

	/**
	 * Constructor for base Authorize.net gateway.
	 */
	public function __construct() {
		if ( ! $this->method_title ) {
			// translators: %s: gateway title
			$this->method_title = sprintf( __( '%s via Authorize.net (PeachPay)', 'peachpay-for-woocommerce' ), $this->title );
		}
		if ( ! $this->method_description ) {
			// translators: %s: gateway title
			$this->method_description = sprintf( __( 'Accept %s payments through Authorize.net', 'peachpay-for-woocommerce' ), $this->title );
		}

		$this->currencies = PeachPay_Authnet_Integration::supported_currencies();
		parent::__construct();

		// Subscription renewal hook — only register if WC Subscriptions is active.
		if ( class_exists( 'WC_Subscriptions_Order' ) ) {
			static $registered = array();
			if ( empty( $registered[ $this->id ] ) ) {
				$gateway = $this;
				add_action(
					'woocommerce_scheduled_subscription_payment_' . $this->id,
					function ( $renewal_total, $renewal_order ) use ( $gateway ) {
						$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
						$subscription  = array_pop( $subscriptions );
						if ( ! $subscription ) {
							$renewal_order->update_status( 'failed', __( 'Authnet renewal failed: no subscription found for renewal order.', 'peachpay-for-woocommerce' ) );
							return;
						}
						$parent_order = wc_get_order( $subscription->get_parent_id() );
						$gateway->process_subscription_renewal( $parent_order, $renewal_order, $renewal_total );
					},
					10,
					2
				);
				$registered[ $this->id ] = true;
			}
		}
	}

	/**
	 * Authorize.net processes payments directly via its own API — no PeachPay session required.
	 *
	 * @return bool
	 */
	protected function requires_peachpay_transaction_id() {
		return false;
	}

	/**
	 * Validates the submitted POST fields on order submission.
	 */
	public function validate_fields() {
		$result = parent::validate_fields();

		// PHPCS:disable WordPress.Security.NonceVerification.Missing
		$data_descriptor = isset( $_POST['peachpay_authnet_data_descriptor'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_authnet_data_descriptor'] ) ) : null;
		$data_value      = isset( $_POST['peachpay_authnet_data_value'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_authnet_data_value'] ) ) : null;
		// PHPCS:enable

		if ( ! $data_descriptor ) {
			wc_add_notice( __( 'Missing required field "peachpay_authnet_data_descriptor"', 'peachpay-for-woocommerce' ), 'error' );
			$result = false;
		}

		if ( ! $data_value ) {
			wc_add_notice( __( 'Missing required field "peachpay_authnet_data_value"', 'peachpay-for-woocommerce' ), 'error' );
			$result = false;
		}

		return $result;
	}

	/**
	 * Process the PeachPay Authorize.net Payment.
	 *
	 * @param int $order_id The id of the order.
	 */
	public function process_payment( $order_id ) {
		try {
			$authnet_mode = PeachPay_Authnet_Integration::mode();
			$order        = parent::process_payment( $order_id );

			if ( ! $order ) {
				return array( 'result' => 'failure' );
			}

			// PHPCS:disable WordPress.Security.NonceVerification.Missing
			$data_descriptor = isset( $_POST['peachpay_authnet_data_descriptor'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_authnet_data_descriptor'] ) ) : null;
			$data_value      = isset( $_POST['peachpay_authnet_data_value'] ) ? sanitize_text_field( wp_unslash( $_POST['peachpay_authnet_data_value'] ) ) : null;
			// PHPCS:enable

			PeachPay_Authnet_Order_Data::set_peachpay_details(
				$order,
				array(
					'peachpay_mode' => peachpay_is_test_mode() ? 'test' : 'live',
					'authnet_mode'  => $authnet_mode,
				)
			);

			// Check for subscription before building params — profile.createProfile must come
			// right after payment per Authorize.net XSD element ordering.
			$is_subscription_order = function_exists( 'peachpay_wcs_order_has_subscription' ) && peachpay_wcs_order_has_subscription( $order );

			// WARNING: The transaction parameters are ordered this way because the API endpoint rejects out of order elements. Order is according to Authorize.net documentation.
			$transaction_params = array(
				'transactionType' => $this->get_option( 'authnet_transaction_type' ),
				'amount'          => PeachPay_Authnet::format_amount( $order->get_total(), $order->get_currency() ),
				'payment'         => array(
					'opaqueData' => array(
						'dataDescriptor' => $data_descriptor,
						'dataValue'      => $data_value,
					),
				),
			);

			// profile must appear right after payment in the Authorize.net schema order.
			if ( $is_subscription_order ) {
				$transaction_params['profile'] = array( 'createProfile' => true );
			}

			$transaction_params['order'] = array(
				'invoiceNumber' => $order->get_order_number(),
				'description'   => self::get_payment_description( $order ),
			);

			if ( PeachPay_Authnet_Advanced::get_setting( 'itemized_order_details' ) === 'yes' ) {
				$transaction_params['lineItems'] = $this->get_authnet_order_line_items( $order );
			}

			$transaction_params['tax'] = array(
				'amount' => $order->get_total_tax(),
			);

			if ( $order->has_shipping_address() ) {
				$transaction_params['shipping'] = array(
					'amount' => $order->get_shipping_total(),
				);
			}

			// customer element must appear before billTo per Authorize.net XSD ordering.
			// Required for CIM profile creation — provides merchantCustomerId + email to Authorize.net.
			if ( $is_subscription_order ) {
				$customer_data = array();
				if ( $order->get_customer_id() ) {
					$customer_data['id'] = (string) $order->get_customer_id();
				}
				if ( $order->get_billing_email() ) {
					$customer_data['email'] = $order->get_billing_email();
				}
				if ( ! empty( $customer_data ) ) {
					$transaction_params['customer'] = $customer_data;
				}
			}

			$transaction_params['billTo'] = array(
				'firstName'   => peachpay_truncate_str( $order->get_billing_first_name(), 50 ),
				'lastName'    => peachpay_truncate_str( $order->get_billing_last_name(), 50 ),
				'address'     => peachpay_truncate_str( $order->get_billing_address_1() . $order->get_billing_address_2(), 60 ),
				'city'        => peachpay_truncate_str( $order->get_billing_city(), 40 ),
				'state'       => peachpay_truncate_str( $order->get_billing_state(), 40 ),
				'zip'         => peachpay_truncate_str( $order->get_billing_postcode(), 20 ),
				'country'     => peachpay_truncate_str( $order->get_billing_country(), 60 ),
				'phoneNumber' => peachpay_truncate_str( $order->get_billing_phone(), 25 ),
			);

			if ( $order->get_billing_company() ) {
				$company = array(
					'company' => peachpay_truncate_str( $order->get_billing_company(), 50 ),
				);
				$transaction_params['billTo'] = array_merge( $transaction_params['billTo'], $company );
			}

			if ( $order->has_shipping_address() ) {
				$transaction_params['shipTo'] = array(
					'firstName' => peachpay_truncate_str( $order->get_shipping_first_name(), 50 ),
					'lastName'  => peachpay_truncate_str( $order->get_shipping_last_name(), 50 ),
					'address'   => peachpay_truncate_str( $order->get_shipping_address_1() . $order->get_shipping_address_2(), 60 ),
					'city'      => peachpay_truncate_str( $order->get_shipping_city(), 40 ),
					'state'     => peachpay_truncate_str( $order->get_shipping_state(), 40 ),
					'zip'       => peachpay_truncate_str( $order->get_shipping_postcode(), 20 ),
					'country'   => peachpay_truncate_str( $order->get_shipping_country(), 60 ),
				);

				if ( $order->get_shipping_company() ) {
					$company = array(
						'company' => peachpay_truncate_str( $order->get_shipping_company(), 50 ),
					);
					$transaction_params['shipTo'] = array_merge( $transaction_params['shipTo'], $company );
				}
			}

			$transaction_params['customerIP'] = $order->get_customer_ip_address();

			$result = PeachPay_Authnet::create_payment(
				$order,
				$transaction_params,
				$this->get_order_details( $order ),
				$authnet_mode
			);

			if ( ! $result ) {
				return array( 'result' => 'failure' );
			}

			// Extract CIM profile IDs from the profileResponse returned alongside the transaction.
			if ( $is_subscription_order ) {
				$profile_response = isset( $result['profile_response'] ) ? $result['profile_response'] : null;
				$profile_ok       = $profile_response && isset( $profile_response['messages']['resultCode'] ) && 'Ok' === $profile_response['messages']['resultCode'];

				if ( $profile_ok ) {
					$customer_profile_id = isset( $profile_response['customerProfileId'] ) ? $profile_response['customerProfileId'] : null;
					$payment_profile_id  = isset( $profile_response['customerPaymentProfileIdList'][0] ) ? $profile_response['customerPaymentProfileIdList'][0] : null;

					if ( $customer_profile_id && $payment_profile_id ) {
						PeachPay_Authnet_Order_Data::set_cim_profile( $order, $customer_profile_id, $payment_profile_id );
					} else {
						peachpay_log( 'warning', 'Authnet profileResponse OK but profile IDs missing.', array( 'order_id' => $order->get_id() ) );
						$order->add_order_note( __( 'Warning: Authorize.net CIM profile could not be created. Subscription renewals may fail. Please check the error log.', 'peachpay-for-woocommerce' ) );
					}
				} else {
					$profile_error_code = $profile_response ? ( isset( $profile_response['messages']['message'][0]['code'] ) ? $profile_response['messages']['message'][0]['code'] : '' ) : '';
					$profile_error_text = $profile_response ? ( isset( $profile_response['messages']['message'][0]['text'] ) ? $profile_response['messages']['message'][0]['text'] : 'unknown' ) : 'no profileResponse in API response';

					if ( 'E00039' === $profile_error_code ) {
						// Customer already has a CIM profile — extract existing ID and fetch payment profile.
						preg_match( '/A duplicate record with ID (\d+) already exists/', $profile_error_text, $matches );
						$existing_profile_id = isset( $matches[1] ) ? $matches[1] : null;

						if ( $existing_profile_id ) {
							$recovered = PeachPay_Authnet::get_customer_profile( $existing_profile_id, $authnet_mode );
							if ( $recovered ) {
								PeachPay_Authnet_Order_Data::set_cim_profile( $order, $recovered['customer_profile_id'], $recovered['payment_profile_id'] );
							} else {
								peachpay_log( 'warning', 'Authnet CIM E00039: could not recover payment profile for customerProfileId ' . $existing_profile_id, array( 'order_id' => $order->get_id() ) );
								$order->add_order_note( __( 'Warning: Authorize.net CIM profile could not be retrieved. Subscription renewals may fail. Please check the error log.', 'peachpay-for-woocommerce' ) );
							}
						} else {
							peachpay_log( 'warning', 'Authnet CIM E00039: could not extract customerProfileId from error text.', array( 'order_id' => $order->get_id(), 'error_text' => $profile_error_text ) );
							$order->add_order_note( __( 'Warning: Authorize.net CIM profile could not be created. Subscription renewals may fail. Please check the error log.', 'peachpay-for-woocommerce' ) );
						}
					} else {
						peachpay_log( 'warning', 'Authnet CIM profile creation failed: ' . $profile_error_text, array( 'order_id' => $order->get_id() ) );
						$order->add_order_note( __( 'Warning: Authorize.net CIM profile could not be created. Subscription renewals may fail. Please check the error log.', 'peachpay-for-woocommerce' ) );
					}
				}

				// Store networkTransId for MIT compliance on future renewals.
				$network_trans_id = isset( $result['network_trans_id'] ) ? $result['network_trans_id'] : null;
				if ( $network_trans_id ) {
					PeachPay_Authnet_Order_Data::set_network_trans_id( $order, $network_trans_id );
				}
			}

			switch ( PeachPay_Authnet_Order_Data::get_transaction_details( $order, 'transactionStatus' ) ) {
				case 'authorizedPendingCapture':
				case 'capturedPendingSettlement':
				case 'settledSuccessfully':
				case 'FDSAuthorizedPendingReview':
				case 'FDSPendingReview':
				case 'underReview':
					return array(
						'result'   => 'success',
						'redirect' => $order->get_checkout_order_received_url(),
					);
				default:
					return array( 'result' => 'failure' );
			}
		} catch ( Exception $exception ) {
			$message = __( 'Error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			if ( function_exists( 'wc_add_notice' ) ) {
				wc_add_notice( $message, 'error' );
			}

			if ( $order ) {
				$order->add_order_note( $message );
			}

			return array( 'result' => 'failure' );
		}
	}

	/**
	 * Handles a WooCommerce Subscriptions scheduled renewal payment.
	 *
	 * Called via the woocommerce_scheduled_subscription_payment_{gateway_id} hook.
	 * Reads the CIM profile from the parent order and charges it. Does NOT call
	 * wc_add_notice() — this runs in a cron context with no user session.
	 *
	 * @param WC_Order $parent_order  The original subscription order with stored CIM profile IDs.
	 * @param WC_Order $renewal_order The new renewal order to be charged.
	 * @param float    $renewal_total The amount to charge.
	 */
	public function process_subscription_renewal( $parent_order, $renewal_order, $renewal_total ) {
		try {
			// Idempotency guard — don't double-charge.
			if ( $renewal_order->is_paid() ) {
				return;
			}

			if ( ! $parent_order ) {
				$renewal_order->update_status( 'failed', __( 'Authnet renewal failed: parent order not found.', 'peachpay-for-woocommerce' ) );
				return;
			}

			$cim_profile = PeachPay_Authnet_Order_Data::get_cim_profile( $parent_order );

			if ( null === $cim_profile ) {
				$renewal_order->update_status(
					'failed',
					sprintf(
						// translators: %d: parent order ID.
						__( 'Authnet renewal failed: CIM profile IDs missing from parent order #%d. The customer may need to re-enter their payment method.', 'peachpay-for-woocommerce' ),
						$parent_order->get_id()
					)
				);
				return;
			}

			$authnet_mode = PeachPay_Authnet_Order_Data::get_peachpay( $parent_order, 'authnet_mode' );
			if ( ! $authnet_mode ) {
				$authnet_mode = PeachPay_Authnet_Integration::mode();
			}

			$result = PeachPay_Authnet::charge_customer_profile(
				$parent_order,
				$cim_profile['customer_profile_id'],
				$cim_profile['payment_profile_id'],
				$renewal_total,
				$authnet_mode
			);

			if ( ! $result['success'] ) {
				$renewal_order->update_status(
					'failed',
					sprintf(
						// translators: %s: failure reason.
						__( 'Authnet subscription renewal failed: %s', 'peachpay-for-woocommerce' ),
						$result['message']
					)
				);
				return;
			}

			PeachPay_Authnet_Order_Data::set_peachpay_details(
				$renewal_order,
				array(
					'peachpay_mode' => peachpay_is_test_mode() ? 'test' : 'live',
					'authnet_mode'  => $authnet_mode,
				)
			);

			PeachPay_Authnet::calculate_payment_state(
				$renewal_order,
				array(
					'transaction_details' => isset( $result['transaction_details'] ) ? $result['transaction_details'] : array(
						'transId'           => $result['trans_id'],
						'transactionStatus' => 'capturedPendingSettlement',
						'settleAmount'      => $renewal_total,
					),
				)
			);

		} catch ( Exception $exception ) {
			$message = __( 'Authnet renewal error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			$renewal_order->add_order_note( $message );
			$renewal_order->update_status( 'failed', $message );
		}
	}

	/**
	 * Process a Authorize.net refund.
	 *
	 * @param  int        $order_id Order Id.
	 * @param  float|null $amount Refund amount.
	 * @param  string     $reason Refund reason.
	 * @return boolean True or false based on success, or a WP_Error object.
	 */
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			// translators: %s the order id.
			return new \WP_Error( 'wc_' . $order_id . '_refund_failed', sprintf( __( 'Refund error: The order_id %s did not match any orders.', 'peachpay-for-woocommerce' ), strval( $order_id ) ) );
		}

		try {

			if ( ! is_numeric( $amount ) || floatval( $amount ) <= 0 ) {
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', __( 'Refund error: Amount must be greater then 0', 'peachpay-for-woocommerce' ) );
			}

			$refund_params = array(
				'transactionType' => 'refundTransaction',
				'amount'          => PeachPay_Authnet::format_amount( $amount, $order->get_currency() ),
			);

			$order_status = PeachPay_Authnet_Order_Data::get_transaction_details( $order, 'transactionStatus' );

			if ( 'capturedPendingSettlement' === $order_status && abs( $amount - $order->get_total() ) >= 0.001 ) {
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', __( 'This transaction cannot be partially refunded until it has settled. You can either wait for settlement to process or use the full order total to void the transaction.', 'peachpay-for-woocommerce' ) );
			} elseif ( 'authorizedPendingCapture' === $order_status ) {
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', __( 'This transaction cannot be partially refunded until it has been captured and settled. Please capture the order or use the full order total to void the transaction.', 'peachpay-for-woocommerce' ) );
			}

			$result = '';
			if ( abs( $amount - $order->get_total() ) < 0.001 && 'capturedPendingSettlement' === $order_status ) {
				$result = PeachPay_Authnet::void_payment( $order );
			} else {
				$result = PeachPay_Authnet::refund_payment( $order, $refund_params );
			}

			if ( ! $result['success'] ) {
				return new \WP_Error( 'wc_' . $order_id . '_refund_failed', ' Refund error: ' . $result['message'] );
			}

			return ( filter_var( $result['success'], FILTER_VALIDATE_BOOLEAN ) );
		} catch ( Exception $exception ) {
			$message = __( 'Refund error: ', 'peachpay-for-woocommerce' ) . $exception->getMessage();
			$order->add_order_note( $message );

			return new \WP_Error( 'wc_' . $order_id . '_refund_failed', $message );
		}
	}

	/**
	 * If Authorize.net is not connected we should prompt the merchant to connect while viewing any Authorize.net gateway.
	 */
	protected function action_needed_form() {
		if ( ! PeachPay_Authnet_Integration::connected() ) {
			?>
			<div class="settings-container action-needed">
				<h1><?php esc_html_e( 'Action needed', 'peachpay-for-woocommerce' ); ?></h1>
				<hr />
				<br />
				<?php require PeachPay::get_plugin_path() . '/core/payments/authnet/admin/views/html-authnet-connect.php'; ?>
			</div>
			<?php
		}
	}

	/**
	 * Authorize.net gateways require Authorize.net to be connected in order to use.
	 *
	 * @param bool $skip_cart_check If true, the cart availability check will be skipped.
	 */
	public function is_available( $skip_cart_check = false ) {
		$is_available = parent::is_available( $skip_cart_check );

		if ( self::needs_setup() ) {
			$is_available = false;
		}

		if ( ! PeachPay_Authnet_Integration::connected() ) {
			$is_available = false;
		}

		return $is_available;
	}

	/**
	 * Authorize.net gateways require setup if Authorize.net is not connected.
	 */
	public function needs_setup() {
		return ! PeachPay_Authnet_Integration::connected();
	}

	/**
	 * Handles fetching the Authorize.net transaction URL
	 *
	 * The woocommerce plugin fetches the url from calling this function on the payment gateway.
	 *
	 * @param order $order Order object related to transaction.
	 * @return string URL linking the transaction Id with the Authorize.net merchant dashboard.
	 */
	public function get_transaction_url( $order ) {
		if ( ! $order->get_transaction_id() ) {
			return '';
		}

		$mode     = PeachPay_Authnet_Order_Data::get_peachpay( $order, 'authnet_mode' );
		$trans_id = PeachPay_Authnet_Order_Data::get_transaction_details( $order, 'transId' );
		$base_url = 'live' === $mode ? 'https://account.authorize.net' : 'https://demo.authorize.net';
		$url      = $base_url . '/smb2/merchant/TransactionManagement/ManageTransactions?transactionId=' . $trans_id;

		return $url;
	}

	/**
	 * Gets the endpoint to callback the store for any payment related status changes.
	 */
	protected function get_callback_url() {
		return get_rest_url( null, 'peachpay/v1/authnet/webhook' );
	}

	/**
	 * Gets the bread crumbs to display on the Authorize.net gateway settings page.
	 */
	protected function get_settings_breadcrumbs() {
		return array(
			array(
				'name' => __( 'Payments', 'peachpay-for-woocommerce' ),
				'url'  => PeachPay_Admin::admin_settings_url( 'peachpay', 'payment', '', '', false ),
			),
			array(
				'name' => __( 'Authorize.net', 'peachpay-for-woocommerce' ),
				'url'  => PeachPay_Admin::admin_settings_url( 'peachpay', 'payment', '', '#' . strtolower( $this->payment_provider ), false ),
			),
			array(
				'name' => $this->title,
			),
		);
	}

	/**
	 * Gets the payment description.
	 *
	 * @param WC_Order $order Order details.
	 * @param boolean  $is_subscription If the description should be for a subscription.
	 */
	public static function get_payment_description( $order, $is_subscription = false ) {
		$prefix = '' !== PeachPay_Authnet_Advanced::get_setting( 'payment_description_prefix' ) ? PeachPay_Authnet_Advanced::get_setting( 'payment_description_prefix' ) : get_bloginfo( 'name' );
		if ( '' !== $prefix ) {
			$prefix = $prefix . ' - ';
		}

		$postfix = '' !== PeachPay_Authnet_Advanced::get_setting( 'payment_description_postfix' ) ? PeachPay_Authnet_Advanced::get_setting( 'payment_description_postfix' ) : '';
		if ( '' !== $postfix ) {
			$postfix = ' ' . $postfix;
		}

		if ( $is_subscription ) {
			return $prefix . 'Subscription Order ' . $order->get_order_number() . $postfix;
		} else {
			return $prefix . 'Order ' . $order->get_order_number() . $postfix;
		}
	}
}
