<?php

namespace SEOPressPro\Actions\Front\Schemas;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use SEOPress\Core\Hooks\ExecuteHooksFrontend;

/**
 * Inject a global merchant return policy (hasMerchantReturnPolicy /
 * MerchantReturnPolicy) into the Organization schema.
 *
 * Google recommends declaring the return policy once at the Organization level
 * rather than repeating it on every product offer
 * (https://developers.google.com/search/docs/appearance/structured-data/return-policy).
 * The policy is configured in SEO > PRO > WooCommerce and stored in
 * seopress_pro_option_name; we emit it through the free plugin's
 * `seopress_get_json_data_organization` filter so no Free change is needed.
 *
 * @since 10.0.0
 */
class MerchantReturnPolicy implements ExecuteHooksFrontend {

	/**
	 * Map our stored values to schema.org enumeration members (without the
	 * scheme, which seopress_check_ssl() prepends).
	 *
	 * @var array<string, array<string, string>>
	 */
	const MAPS = array(
		'category'  => array(
			'finite'        => 'MerchantReturnFiniteReturnWindow',
			'unlimited'     => 'MerchantReturnUnlimitedWindow',
			'not_permitted' => 'MerchantReturnNotPermitted',
		),
		'method'    => array(
			'by_mail'  => 'ReturnByMail',
			'in_store' => 'ReturnInStore',
			'at_kiosk' => 'ReturnAtKiosk',
		),
		'fees'      => array(
			'free'     => 'FreeReturn',
			'customer' => 'ReturnFeesCustomerResponsibility',
			'shipping' => 'ReturnShippingFees',
		),
		'refund'    => array(
			'full'         => 'FullRefund',
			'exchange'     => 'ExchangeRefund',
			'store_credit' => 'StoreCreditRefund',
		),
		'condition' => array(
			'new'         => 'NewCondition',
			'used'        => 'UsedCondition',
			'refurbished' => 'RefurbishedCondition',
			'damaged'     => 'DamagedCondition',
		),
	);

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function hooks() {
		add_filter( 'seopress_get_json_data_organization', array( $this, 'addReturnPolicy' ) );
	}

	/**
	 * Append hasMerchantReturnPolicy to the Organization schema.
	 *
	 * @param array $data The Organization schema data.
	 *
	 * @return array
	 */
	public function addReturnPolicy( $data ) {
		$options = get_option( 'seopress_pro_option_name' );
		if ( ! is_array( $options ) ) {
			return $data;
		}

		if ( empty( $options['seopress_woocommerce_return_policy_enable'] ) ) {
			return $data;
		}

		// Required: at least one applicable country + a policy category.
		$countries = $this->parseCountries( isset( $options['seopress_woocommerce_return_policy_applicable_country'] ) ? $options['seopress_woocommerce_return_policy_applicable_country'] : '' );
		$category  = isset( $options['seopress_woocommerce_return_policy_category'] ) ? $options['seopress_woocommerce_return_policy_category'] : '';
		if ( empty( $countries ) || empty( self::MAPS['category'][ $category ] ) ) {
			return $data;
		}

		$ssl = function_exists( 'seopress_check_ssl' ) ? seopress_check_ssl() : 'https://';

		$policy = array(
			'@type'                => 'MerchantReturnPolicy',
			'applicableCountry'    => $countries,
			'returnPolicyCategory' => $ssl . 'schema.org/' . self::MAPS['category'][ $category ],
		);

		// merchantReturnDays is required for a finite return window.
		if ( 'finite' === $category ) {
			$days = isset( $options['seopress_woocommerce_return_policy_days'] ) ? (int) $options['seopress_woocommerce_return_policy_days'] : 0;
			if ( $days > 0 ) {
				$policy['merchantReturnDays'] = $days;
			}
		}

		$country = isset( $options['seopress_woocommerce_return_policy_country'] ) ? trim( $options['seopress_woocommerce_return_policy_country'] ) : '';
		if ( '' !== $country ) {
			$policy['returnPolicyCountry'] = strtoupper( $country );
		}

		$this->addEnum( $policy, 'returnMethod', 'method', $options, 'seopress_woocommerce_return_policy_method', $ssl );
		$this->addEnum( $policy, 'returnFees', 'fees', $options, 'seopress_woocommerce_return_policy_fees', $ssl );
		$this->addEnum( $policy, 'refundType', 'refund', $options, 'seopress_woocommerce_return_policy_refund_type', $ssl );
		$this->addEnum( $policy, 'itemCondition', 'condition', $options, 'seopress_woocommerce_return_policy_item_condition', $ssl );

		// returnShippingFeesAmount is meaningful only when the customer pays the
		// return shipping.
		$fees = isset( $options['seopress_woocommerce_return_policy_fees'] ) ? $options['seopress_woocommerce_return_policy_fees'] : '';
		if ( 'shipping' === $fees ) {
			$amount = isset( $options['seopress_woocommerce_return_policy_fees_amount'] ) ? $options['seopress_woocommerce_return_policy_fees_amount'] : '';
			if ( is_numeric( $amount ) ) {
				$policy['returnShippingFeesAmount'] = array(
					'@type'    => 'MonetaryAmount',
					'value'    => (float) $amount,
					'currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD',
				);
			}
		}

		$data['hasMerchantReturnPolicy'] = apply_filters( 'seopress_pro_merchant_return_policy', $policy, $options );

		return $data;
	}

	/**
	 * Add a schema.org enumeration value to the policy when the option is set.
	 *
	 * @param array  $policy   The policy array (by reference).
	 * @param string $prop     The schema property name.
	 * @param string $map      The MAPS sub-key.
	 * @param array  $options  The plugin options.
	 * @param string $opt_key  The option key holding the stored value.
	 * @param string $ssl      The scheme prefix.
	 *
	 * @return void
	 */
	private function addEnum( &$policy, $prop, $map, $options, $opt_key, $ssl ) {
		$value = isset( $options[ $opt_key ] ) ? $options[ $opt_key ] : '';
		if ( ! empty( self::MAPS[ $map ][ $value ] ) ) {
			$policy[ $prop ] = $ssl . 'schema.org/' . self::MAPS[ $map ][ $value ];
		}
	}

	/**
	 * Normalize the stored applicable countries into a clean array of ISO codes.
	 *
	 * Accepts the array stored by the country picker, or a legacy
	 * comma-separated string.
	 *
	 * @param mixed $raw The stored value (array or comma-separated string).
	 *
	 * @return array
	 */
	private function parseCountries( $raw ) {
		if ( is_array( $raw ) ) {
			$list = $raw;
		} elseif ( is_string( $raw ) && '' !== trim( $raw ) ) {
			$list = explode( ',', $raw );
		} else {
			return array();
		}

		$codes = array_map(
			function ( $code ) {
				return strtoupper( trim( (string) $code ) );
			},
			$list
		);

		// Keep only plausible two-letter codes, capped at Google's 50 limit.
		$codes = array_values(
			array_unique(
				array_filter(
					$codes,
					function ( $code ) {
						return (bool) preg_match( '/^[A-Z]{2}$/', $code );
					}
				)
			)
		);

		return array_slice( $codes, 0, 50 );
	}
}
