<?php
/**
 * Payouts Service Module Functions
 *
 * Wires the Payouts Service into the async batch payout flow (2.33.0+).
 *
 * @package    AffiliateWP
 * @subpackage Payouts/Methods/PayoutsService
 * @since      2.33.0
 */

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

/**
 * Whether the Payouts Service can currently be used to pay a batch.
 *
 * Enabled toggle on, connection active, credentials present, not past sunset.
 *
 * @since 2.33.0
 *
 * @return bool
 */
function affwp_payouts_service_batch_can_pay() {

	if ( function_exists( 'affwp_is_payouts_service_sunset_passed' ) && affwp_is_payouts_service_sunset_passed() ) {
		return false;
	}

	if ( ! affiliate_wp()->settings->get( 'enable_payouts_service', false ) ) {
		return false;
	}

	$vendor_id         = affiliate_wp()->settings->get( 'payouts_service_vendor_id', 0 );
	$access_key        = affiliate_wp()->settings->get( 'payouts_service_access_key', '' );
	$connection_status = affiliate_wp()->settings->get( 'payouts_service_connection_status', '' );

	return 'active' === $connection_status && $vendor_id && $access_key;
}

/**
 * Starts Payouts Service payouts in 'processing' so the batch row stays pending
 * until the service confirms. No-op for other methods or when PS can't pay.
 *
 * @since 2.33.0
 *
 * @param string $status        Initial payout status.
 * @param string $payout_method Method identifier.
 * @return string
 */
function affwp_payouts_service_filter_batch_initial_status( $status, $payout_method ) {

	if ( 'payouts-service' !== $payout_method ) {
		return $status;
	}

	if ( ! affwp_payouts_service_batch_can_pay() ) {
		return $status;
	}

	return 'processing';
}

/**
 * Sends a bulk payout request to the Payouts Service (POST /payout).
 *
 * Shared by the legacy generate flow and the async batch flow; loaded in every
 * context so AS/cron workers can reach it without the admin-only class.
 *
 * @since 2.33.0
 *
 * @param array $payouts Payout data keyed by affiliate ID. Each entry:
 *                        account_id, affiliate_id, amount, referrals.
 * @return object|\WP_Error Decoded response on success, WP_Error on failure.
 */
function affwp_payouts_service_send_payout_request( $payouts = array() ) {

	$body_args = array(
		'payout_data'   => $payouts,
		'currency'      => affwp_get_currency(),
		'affwp_version' => AFFILIATEWP_VERSION,
	);

	$args = array(
		'body'      => $body_args,
		'headers'   => affwp_get_payouts_service_http_headers(),
		'timeout'   => 60,
		'sslverify' => false,
	);

	affiliate_wp()->utils->log( 'affwp_payouts_service_send_payout_request()', $body_args );

	$request = wp_remote_post( PAYOUTS_SERVICE_URL . '/wp-json/payouts/v1/payout', $args );

	if ( is_wp_error( $request ) ) {
		affiliate_wp()->utils->log( 'affwp_payouts_service_send_payout_request() transport error', $request );
		return $request;
	}

	$response      = json_decode( wp_remote_retrieve_body( $request ) );
	$response_code = (int) wp_remote_retrieve_response_code( $request );

	if ( 200 !== $response_code ) {
		$message = ( is_object( $response ) && ! empty( $response->message ) )
			? $response->message
			: __( 'Unable to process the Payouts Service request at the moment. Please try again later.', 'affiliate-wp' );

		$error = new \WP_Error( $response_code ? $response_code : 'payouts_service_error', $message );

		affiliate_wp()->utils->log( 'affwp_payouts_service_send_payout_request() request failed', $error );

		return $error;
	}

	return $response;
}

/**
 * Resolves a failure class for a Payouts Service payout, guarded so the batch
 * keeps working if the classifier registry isn't loaded.
 *
 * @since 2.33.0
 *
 * @param string|\WP_Error           $code    Error code (or WP_Error).
 * @param string                     $message Human-readable error message.
 * @param \AffWP\Affiliate\Payout    $payout  Payout context.
 * @return string|null
 */
function affwp_payouts_service_failure_class( $code, $message, $payout ) {

	if ( ! class_exists( '\AffWP\Payouts\Failure_Classifier' ) ) {
		return null;
	}

	return \AffWP\Payouts\Failure_Classifier::classify( 'payouts-service', $code, $message, $payout );
}

/**
 * Pays every 'processing' Payouts Service payout in a completed batch via one
 * bulk request, distributing the service payout id + invoice link across rows.
 *
 * Idempotent: only reprocesses rows still in 'processing'.
 *
 * @since 2.33.0
 *
 * @param int $batch_id Payout batch ID.
 * @return void
 */
function affwp_payouts_service_process_completed_batch( $batch_id ) {

	$batch_id = absint( $batch_id );

	if ( empty( $batch_id ) || ! affwp_payouts_service_batch_can_pay() ) {
		return;
	}

	$payouts = affiliate_wp()->affiliates->payouts->get_payouts(
		array(
			'batch_id'      => $batch_id,
			'payout_method' => 'payouts-service',
			'status'        => 'processing',
			'number'        => -1,
		)
	);

	if ( empty( $payouts ) ) {
		return;
	}

	$request_payload = array();
	$valid_payouts   = array();

	foreach ( $payouts as $payout ) {

		// Already handed off (has a service id): skip so the watchdog re-firing this hook can't create a duplicate payout.
		if ( ! empty( $payout->service_id ) ) {
			continue;
		}

		$affiliate_id = (int) $payout->affiliate_id;
		$account      = affwp_get_payouts_service_account( $affiliate_id );

		if ( empty( $account['valid'] ) || empty( $account['account_id'] ) ) {

			affiliate_wp()->affiliates->payouts->update(
				$payout->payout_id,
				array(
					'status'        => 'failed',
					'description'   => __( 'Affiliate has no valid Payouts Service account.', 'affiliate-wp' ),
					'failure_class' => affwp_payouts_service_failure_class( 'no_account', '', $payout ),
				),
				'',
				'payout'
			);

			continue;
		}

		$request_payload[ $affiliate_id ] = array(
			'account_id'   => $account['account_id'],
			'affiliate_id' => $affiliate_id,
			'amount'       => $payout->amount,
			'referrals'    => array_filter( array_map( 'intval', explode( ',', (string) $payout->referrals ) ) ),
		);

		$valid_payouts[ $affiliate_id ] = $payout;
	}

	if ( empty( $request_payload ) ) {
		affiliate_wp()->affiliates->payout_batches->recount( $batch_id );
		return;
	}

	$response = affwp_payouts_service_send_payout_request( $request_payload );

	if ( is_wp_error( $response ) ) {

		$message = $response->get_error_message();

		foreach ( $valid_payouts as $payout ) {
			affiliate_wp()->affiliates->payouts->update(
				$payout->payout_id,
				array(
					'status'        => 'failed',
					'description'   => $message,
					'failure_class' => affwp_payouts_service_failure_class( $response->get_error_code(), $message, $payout ),
				),
				'',
				'payout'
			);
		}

		affiliate_wp()->affiliates->payout_batches->recount( $batch_id );
		return;
	}

	// Rows stay 'processing'; the service confirms each to paid/failed later, matched on service_id + affiliate_id.
	$service_id   = isset( $response->payout_id ) ? (int) $response->payout_id : 0;
	$invoice_link = isset( $response->payment_link ) ? esc_url_raw( $response->payment_link ) : '';

	foreach ( $valid_payouts as $affiliate_id => $payout ) {

		affiliate_wp()->affiliates->payouts->update(
			$payout->payout_id,
			array(
				'status'               => 'processing',
				'service_id'           => $service_id,
				'service_invoice_link' => $invoice_link,
				'service_account'      => affwp_payouts_service_payout_account_label( $affiliate_id ),
				'failure_class'        => null,
			),
			'',
			'payout'
		);
	}

	affiliate_wp()->affiliates->payout_batches->recount( $batch_id );
}

/**
 * Human-readable payout destination label for a Payouts Service affiliate
 * (bank name + masked account, or card description).
 *
 * @since 2.33.0
 *
 * @param int $affiliate_id Affiliate ID.
 * @return string
 */
function affwp_payouts_service_payout_account_label( $affiliate_id ) {

	$method = affwp_get_affiliate_meta( $affiliate_id, 'payouts_service_payout_method', true );

	if ( ! is_array( $method ) ) {
		return '';
	}

	if ( isset( $method['payout_method'] ) && 'bank_account' === $method['payout_method'] ) {
		return $method['bank_name'] . ' (' . $method['account_no'] . ')';
	}

	return isset( $method['card'] ) ? $method['card'] : '';
}

/**
 * Syncs a payout's referrals to match a service-confirmed payout status.
 *
 * Batch payouts start 'processing' with referrals left 'unpaid'; the service
 * confirms later. On 'paid' the referrals must follow, otherwise the affiliate
 * stays liable for already-paid earnings.
 *
 * @since 2.33.0
 *
 * @param \AffWP\Affiliate\Payout|object $payout Payout row (needs ->referrals).
 * @param string                         $status Confirmed payout status.
 * @return void
 */
function affwp_payouts_service_sync_referral_statuses( $payout, $status ) {

	if ( 'paid' !== $status || empty( $payout->referrals ) ) {
		return;
	}

	foreach ( array_filter( array_map( 'intval', explode( ',', (string) $payout->referrals ) ) ) as $referral_id ) {
		affwp_set_referral_status( $referral_id, 'paid' );
	}
}
